diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/.eslintrc.js b/packages/@aws-cdk/app-staging-synthesizer-alpha/.eslintrc.js new file mode 100644 index 0000000000000..c6b0adb2216b1 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/.eslintrc.js @@ -0,0 +1,4 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.ignorePatterns.push('resources/**/*'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/.gitignore b/packages/@aws-cdk/app-staging-synthesizer-alpha/.gitignore new file mode 100644 index 0000000000000..1272e8254630e --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/.gitignore @@ -0,0 +1,22 @@ +*.js +*.d.ts +tsconfig.json +*.generated.ts +*.js.map +dist +coverage +.nyc_output +.jsii + +.LAST_BUILD +nyc.config.js +.LAST_PACKAGE +*.snk +!.eslintrc.js + +junit.xml +!jest.config.js +!**/*.snapshot/**/asset.*/*.js +!**/*.snapshot/**/asset.*/*.d.ts + +!**/*.snapshot/**/asset.*/** diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/.npmignore b/packages/@aws-cdk/app-staging-synthesizer-alpha/.npmignore new file mode 100644 index 0000000000000..773d1bc0f120e --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/.npmignore @@ -0,0 +1,14 @@ + +.LAST_BUILD +*.snk +junit.xml +*.ts +!*.d.ts +!*.js +!*.lit.ts +coverage +.nyc_output +*.tgz +.eslintrc.js +# exclude cdk artifacts +**/cdk.out \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/LICENSE b/packages/@aws-cdk/app-staging-synthesizer-alpha/LICENSE new file mode 100644 index 0000000000000..9b722c65c5481 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/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/app-staging-synthesizer-alpha/NOTICE b/packages/@aws-cdk/app-staging-synthesizer-alpha/NOTICE new file mode 100644 index 0000000000000..a27b7dd317649 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/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/app-staging-synthesizer-alpha/README.md b/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md new file mode 100644 index 0000000000000..9d9c9e372f7a0 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/README.md @@ -0,0 +1,389 @@ +# App Staging Synthesizer + + +--- + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + +--- + + + +This library includes constructs aimed at replacing the current model of bootstrapping and providing +greater control of the bootstrap experience to the CDK user. The important constructs in this library +are as follows: + +- the `IStagingResources` interface: a framework for an app-level bootstrap stack that handles + file assets and docker assets. +- the `DefaultStagingStack`, which is a works-out-of-the-box implementation of the `IStagingResources` + interface. +- the `AppStagingSynthesizer`, a new CDK synthesizer that will synthesize CDK applications with + the staging resources provided. + +> Currently this module does not support CDK Pipelines. You must deploy CDK Apps using this +> synthesizer via `cdk deploy`. + +To get started, update your CDK App with a new `defaultStackSynthesizer`: + +```ts +const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: 'my-app-id', // put a unique id here + }), +}); +``` + +This will introduce a `DefaultStagingStack` in your CDK App and staging assets of your App +will live in the resources from that stack rather than the CDK Bootstrap stack. + +If you are migrating from a different version of synthesis your updated CDK App will target +the resources in the `DefaultStagingStack` and no longer be tied to the bootstrapped resources +in your account. + +## Bootstrap Model + +Our current bootstrap model looks like this, when you run `cdk bootstrap aws:///` : + +```text +┌───────────────────────────────────┐┌────────────────────────┐┌────────────────────────┐ +│ ││ ││ │ +│ ││ ││ │ +│ ┌───────────────┐ ││ ┌──────────────┐ ││ ┌──────────────┐ │ +│ │Bootstrap Stack│ ││ │ CDK App 1 │ ││ │ CDK App 2 │ │ +│ └───────────────┘ ││ └──────────────┘ ││ └──────────────┘ │ +│ ││ ││ │ +│ ││ ││ │ +│ ┌───────────────────────────┐ ││ ┌────────────┐ ││ │ +│ │IAM Role for CFN execution │ ││┌────│ S3 Asset │ ││ │ +│ │ IAM Role for lookup │ │││ └────────────┘ ││ │ +│ │ IAM Role for deployment │ │││ ││ │ +│ └───────────────────────────┘ │││ ││ ┌─────────────┐ │ +│ │││ ┌──────────┼┼─────│ S3 Asset │ │ +│ │││ │ ││ └─────────────┘ │ +│ ┌───────────────────────────────┐ │││ │ ││ │ +│ │ IAM Role for File Publishing │ │││ │ ││ │ +│ │ IAM Role for Image Publishing │ │││ │ ││ │ +│ └───────────────────────────────┘ │││ │ ││ │ +│ │││ │ ││ │ +│ ┌─────────────────────────────┐ │││ │ ││ │ +│ │S3 Bucket for Staging Assets │ │││ │ ││ │ +│ │ KMS Key encryption │◀─┼┼┴────────────┘ ││ ┌────────────┐ │ +│ └─────────────────────────────┘ ││ ┌──────────┼┼───── │ ECR Asset │ │ +│ ││ │ ││ └────────────┘ │ +│ ││ │ ││ │ +│┌─────────────────────────────────┐││ │ ││ │ +││ECR Repository for Staging Assets◀┼┼─────────────┘ ││ │ +│└─────────────────────────────────┘││ ││ │ +│ ││ ││ │ +│ ││ ││ │ +│ ││ ││ │ +│ ││ ││ │ +│ ││ ││ │ +│ ││ ││ │ +└───────────────────────────────────┘└────────────────────────┘└────────────────────────┘ +``` + +Your CDK Application utilizes these resources when deploying. For example, if you have a file asset, +it gets uploaded to the S3 Staging Bucket using the File Publishing Role when you run `cdk deploy`. + +This library introduces an alternate model to bootstrapping, by splitting out essential CloudFormation IAM roles +and staging resources. There will still be a Bootstrap Stack, but this will only contain IAM roles necessary for +CloudFormation deployment. Each CDK App will instead be in charge of its own staging resources, including the +S3 Bucket, ECR Repositories, and associated IAM roles. It works like this: + +The Staging Stack will contain, on a per-need basis, + +- 1 S3 Bucket with KMS encryption for all file assets in the CDK App. +- An ECR Repository _per_ image (and its revisions). +- IAM roles with access to the Bucket and Repositories. + +```text +┌─────────────────────────────┐┌───────────────────────────────────────┐┌───────────────────────────────────────┐ +│ ││ ││ │ +│ ┌───────────────┐ ││ ┌──────────────┐ ││ ┌──────────────┐ │ +│ │Bootstrap Stack│ ││ │ CDK App 1 │ ││ │ CDK App 2 │ │ +│ └───────────────┘ ││ └──────────────┘ ││ └──────────────┘ │ +│ ││┌──────────────────┐ ││┌──────────────────┐ │ +│ │││ ┌──────────────┐ │ │││ ┌──────────────┐ │ │ +│ │││ │Staging Stack │ │ │││ │Staging Stack │ │ │ +│ │││ └──────────────┘ │ │││ └──────────────┘ │ │ +│ │││ │ │││ │ │ +│ │││ │ │││ │ │ +│ │││┌────────────────┐│ ┌────────────┐│││┌────────────────┐│ ┌────────────┐│ +│ ││││ IAM Role for ││ ┌───│ S3 Asset │││││ IAM Role for ││ ┌───│ S3 Asset ││ +│ ││││File Publishing ││ │ └────────────┘││││File Publishing ││ │ └────────────┘│ +│ │││└────────────────┘│ │ ││││ IAM Role for ││ │ │ +│ │││ │ │ ││││Image Publishing││ │ │ +│┌───────────────────────────┐│││ │ │ │││└────────────────┘│ │ │ +││IAM Role for CFN execution ││││ │ │ │││ │ │ │ +││ IAM Role for lookup ││││ │ │ │││ │ │ │ +││ IAM Role for deployment ││││┌────────────────┐│ │ │││┌────────────────┐│ │ │ +│└───────────────────────────┘││││ S3 Bucket for ││ │ ││││ S3 Bucket for ││ │ │ +│ ││││ Staging Assets │◀─┘ ││││ Staging Assets │◀─┘ │ +│ │││└────────────────┘│ │││└────────────────┘│ ┌───────────┐│ +│ │││ │ │││ │ ┌───│ ECR Asset ││ +│ │││ │ │││┌────────────────┐│ │ └───────────┘│ +│ │││ │ ││││ ECR Repository ││ │ │ +│ │││ │ ││││ for Staging │◀──┘ │ +│ │││ │ ││││ Assets ││ │ +│ │││ │ │││└────────────────┘│ │ +│ │││ │ │││ │ │ +│ │││ │ │││ │ │ +│ │││ │ │││ │ │ +│ │││ │ │││ │ │ +│ │││ │ │││ │ │ +│ ││└──────────────────┘ ││└──────────────────┘ │ +└─────────────────────────────┘└───────────────────────────────────────┘└───────────────────────────────────────┘ +``` + +This allows staging resources to be created when needed next to the CDK App. It has the following +benefits: + +- Resources between separate CDK Apps are separated so they can be cleaned up and lifecycle +controlled individually. +- Users have a familiar way to customize staging resources in the CDK Application. + +> As this library is `experimental`, the accompanying Bootstrap Stack is not yet implemented. To use this +> library right now, you must reuse roles that have been traditionally bootstrapped. + +## Using the Default Staging Stack per Environment + +The most common use case will be to use the built-in default resources. In this scenario, the +synthesizer will create a new Staging Stack in each environment the CDK App is deployed to store +its staging resources. To use this kind of synthesizer, use `AppStagingSynthesizer.defaultResources()`. + +```ts +const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: 'my-app-id', + }), +}); +``` + +Every CDK App that uses the `DefaultStagingStack` must include an `appId`. This should +be an identifier unique to the app and is used to differentiate staging resources associated +with the app. + +### Default Staging Stack + +The Default Staging Stack includes all the staging resources necessary for CDK Assets. The below example +is of a CDK App using the `AppStagingSynthesizer` and creating a file asset for the Lambda Function +source code. As part of the `DefaultStagingStack`, an S3 bucket and IAM role will be created that will be +used to upload the asset to S3. + +```ts +const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id' }), +}); + +const stack = new Stack(app, 'my-stack'); + +new lambda.Function(stack, 'lambda', { + code: lambda.AssetCode.fromAsset(path.join(__dirname, 'assets')), + handler: 'index.handler', + runtime: lambda.Runtime.PYTHON_3_9, +}); + +app.synth(); +``` + +### Custom Roles + +You can customize some or all of the roles you'd like to use in the synthesizer as well, +if all you need is to supply custom roles (and not change anything else in the `DefaultStagingStack`): + +```ts +const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: 'my-app-id', + deploymentIdentities: DeploymentIdentities.specifyRoles({ + cloudFormationExecutionRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Execute'), + deploymentRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Deploy'), + lookupRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/Lookup'), + }), + }), +}); +``` + +Or, you can ask to use the CLI credentials that exist at deploy-time. +These credentials must have the ability to perform CloudFormation calls, +lookup resources in your account, and perform CloudFormation deployment. +For a full list of what is necessary, see `LookupRole`, `DeploymentActionRole`, +and `CloudFormationExecutionRole` in the +[bootstrap template](https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/lib/api/bootstrap/bootstrap-template.yaml). + +```ts +const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: 'my-app-id', + deploymentIdentities: DeploymentIdentities.cliCredentials(), + }), +}); +``` + +The default staging stack will create roles to publish to the S3 bucket and ECR repositories, +assumable by the deployment role. You can also specify an existing IAM role for the +`fileAssetPublishingRole` or `imageAssetPublishingRole`: + +```ts +const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: 'my-app-id', + fileAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/S3Access'), + imageAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/ECRAccess'), + }), +}); +``` + +### Deploy Time S3 Assets + +There are two types of assets: + +- Assets used only during deployment. These are used to hand off a large piece of data to another +service, that will make a private copy of that data. After deployment, the asset is only necessary for +a potential future rollback. +- Assets accessed throughout the running life time of the application. + +Examples of assets that are only used at deploy time are CloudFormation Templates and Lambda Code +bundles. Examples of assets accessed throughout the life time of the application are script files +downloaded to run in a CodeBuild Project, or on EC2 instance startup. ECR images are always application +life-time assets. S3 deploy time assets are stored with a `deploy-time/` prefix, and a lifecycle rule will collect them after a configurable number of days. + +Lambda assets are by default marked as deploy time assets: + +```ts +declare const stack: Stack; +new lambda.Function(stack, 'lambda', { + code: lambda.AssetCode.fromAsset(path.join(__dirname, 'assets')), // lambda marks deployTime = true + handler: 'index.handler', + runtime: lambda.Runtime.PYTHON_3_9, +}); +``` + +Or, if you want to create your own deploy time asset: + +```ts +import { Asset } from 'aws-cdk-lib/aws-s3-assets'; + +declare const stack: Stack; +const asset = new Asset(stack, 'deploy-time-asset', { + deployTime: true, + path: path.join(__dirname, './deploy-time-asset'), +}); +``` + +By default, we store deploy time assets for 30 days, but you can change this number by specifying +`deployTimeFileAssetLifetime`. The number you specify here is how long you will be able to roll back +to a previous version of an application just by doing a CloudFormation deployment with the old +template, without rebuilding and republishing assets. + +```ts +const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: 'my-app-id', + deployTimeFileAssetLifetime: Duration.days(100), + }), +}); +``` + +### Lifecycle Rules on ECR Repositories + +By default, we store a maximum of 3 revisions of a particular docker image asset. This allows +for smooth faciliation of rollback scenarios where we may reference previous versions of an +image. When more than 3 revisions of an asset exist in the ECR repository, the oldest one is +purged. + +To change the number of revisions stored, use `imageAssetVersionCount`: + +```ts +const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: 'my-app-id', + imageAssetVersionCount: 10, + }), +}); +``` + +## Using a Custom Staging Stack per Environment + +If you want to customize some behavior that is not configurable via properties, +you can implement your own class that implements `IStagingResources`. To get a head start, +you can subclass `DefaultStagingStack`. + +```ts +interface CustomStagingStackOptions extends DefaultStagingStackOptions {} + +class CustomStagingStack extends DefaultStagingStack { +} +``` + +Or you can roll your own staging resources from scratch, as long as it implements `IStagingResources`. + +```ts +interface CustomStagingStackProps extends StackProps {} + +class CustomStagingStack extends Stack implements IStagingResources { + public constructor(scope: Construct, id: string, props: CustomStagingStackProps) { + super(scope, id, props); + } + + public addFile(asset: FileAssetSource): FileStagingLocation { + return { + bucketName: 'myBucket', + assumeRoleArn: 'myArn', + dependencyStack: this, + }; + } + + public addDockerImage(asset: DockerImageAssetSource): ImageStagingLocation { + return { + repoName: 'myRepo', + assumeRoleArn: 'myArn', + dependencyStack: this, + }; + } +} +``` + +Using your custom staging resources means implementing a `CustomFactory` class and calling the +`AppStagingSynthesizer.customFactory()` static method. This has the benefit of providing a +custom Staging Stack that can be created in every environment the CDK App is deployed to. + +```ts fixture=with-custom-staging +class CustomFactory implements IStagingResourcesFactory { + public obtainStagingResources(stack: Stack, context: ObtainStagingResourcesContext) { + const myApp = App.of(stack); + + return new CustomStagingStack(myApp!, `CustomStagingStack-${context.environmentString}`, {}); + } +} + +const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.customFactory({ + factory: new CustomFactory(), + oncePerEnv: true, // by default + }), +}); +``` + +## Using an Existing Staging Stack + +Use `AppStagingSynthesizer.customResources()` to supply an existing stack as the Staging Stack. +Make sure that the custom stack you provide implements `IStagingResources`. + +```ts fixture=with-custom-staging +const resourceApp = new App(); +const resources = new CustomStagingStack(resourceApp, 'CustomStagingStack', {}); + +const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.customResources({ + resources, + }), +}); +``` diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/adr/resource-names.md b/packages/@aws-cdk/app-staging-synthesizer-alpha/adr/resource-names.md new file mode 100644 index 0000000000000..f718afc5ca0e6 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/adr/resource-names.md @@ -0,0 +1,30 @@ +# Staging Stack Resource Names + +The Staging Stack can produce the following types of resources, depending on what is needed for the app: + +- iam role (file publishing role and asset publishing role) +- s3 bucket (one per app) +- ecr repository (one per image asset family) + +These resources need to be named unique to their scope to avoid CloudFormation errors when trying to create +a resource with an existing name. The resource specific limitations are as follows: + +- iam role names: must be unique to their account +- s3 bucket names: must be globally unique +- ecr repository names: must be unique to their account/region + +The attributes we can use to name our resources are as follows: + +- account number (i.e. `123456789012`) +- region name (i.e. `us-east-1`) +- app id (a user-specified id that should be unique to the app) +- image id (a user-specified id added on image assets) + +This information can be distilled into the following table, which shows what identifiers are necessary to +make each resource name unique: + +| Resource | Account | Region | App Id | Image Id | +| --------- | ------- | ------ | ------ | -------- | +| iam roles | | ✔️ | ✔️ | | +| s3 bucket | ✔️ | ✔️ | ✔️ ️️ | | +| ecr repos | | | ✔️ | ✔️ | diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/jest.config.js b/packages/@aws-cdk/app-staging-synthesizer-alpha/jest.config.js new file mode 100644 index 0000000000000..87e3ed1d7117c --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/jest.config.js @@ -0,0 +1,4 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, +}; diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts new file mode 100644 index 0000000000000..21f7290d19f4b --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts @@ -0,0 +1,372 @@ +import { + AssetManifestBuilder, + BOOTSTRAP_QUALIFIER_CONTEXT, + DockerImageAssetLocation, + DockerImageAssetSource, + FileAssetLocation, + FileAssetSource, + IBoundStackSynthesizer as IBoundAppStagingSynthesizer, + IReusableStackSynthesizer, + ISynthesisSession, + Stack, + StackSynthesizer, + Token, +} from 'aws-cdk-lib'; +import { StringSpecializer, translateCfnTokenToAssetToken } from 'aws-cdk-lib/core/lib/helpers-internal'; +import { BootstrapRole, BootstrapRoles, DeploymentIdentities } from './bootstrap-roles'; +import { DefaultStagingStack, DefaultStagingStackOptions } from './default-staging-stack'; +import { PerEnvironmentStagingFactory as PerEnvironmentStagingFactory } from './per-env-staging-factory'; +import { AppScopedGlobal } from './private/app-global'; +import { validateNoTokens } from './private/no-tokens'; +import { IStagingResources, IStagingResourcesFactory, ObtainStagingResourcesContext } from './staging-stack'; + +const AGNOSTIC_STACKS = new AppScopedGlobal(() => new Set()); +const ENV_AWARE_STACKS = new AppScopedGlobal(() => new Set()); + +/** + * Options that apply to all AppStagingSynthesizer variants + */ +export interface AppStagingSynthesizerOptions { + /** + * What roles to use to deploy applications + * + * These are the roles that have permissions to interact with CloudFormation + * on your behalf. By default these are the standard bootstrapped CDK roles, + * but you can customize them or turn them off and use the CLI credentials + * to deploy. + * + * @default - The standard bootstrapped CDK roles + */ + readonly deploymentIdentities?: DeploymentIdentities; + + /** + * Qualifier to disambiguate multiple bootstrapped environments in the same account + * + * This qualifier is only used to reference bootstrapped resources. It will not + * be used in the creation of app-specific staging resources: `appId` is used for that + * instead. + * + * @default - Value of context key '@aws-cdk/core:bootstrapQualifier' if set, otherwise `DEFAULT_QUALIFIER` + */ + readonly bootstrapQualifier?: string; +} + +/** + * Properties for stackPerEnv static method + */ +export interface DefaultResourcesOptions extends AppStagingSynthesizerOptions, DefaultStagingStackOptions {} + +/** + * Properties for customFactory static method + */ +export interface CustomFactoryOptions extends AppStagingSynthesizerOptions { + /** + * The factory that will be used to return staging resources for each stack + */ + readonly factory: IStagingResourcesFactory; + + /** + * Reuse the answer from the factory for stacks in the same environment + * + * @default true + */ + readonly oncePerEnv?: boolean; +} + +/** + * Properties for customResources static method + */ +export interface CustomResourcesOptions extends AppStagingSynthesizerOptions { + /** + * Use these exact staging resources for every stack that this synthesizer is used for + */ + readonly resources: IStagingResources; +} + +/** + * Internal properties for AppStagingSynthesizer + */ +interface AppStagingSynthesizerProps extends AppStagingSynthesizerOptions { + /** + * A factory method that creates an IStagingStack when given the stack the + * synthesizer is binding. + */ + readonly factory: IStagingResourcesFactory; +} + +/** + * App Staging Synthesizer + */ +export class AppStagingSynthesizer extends StackSynthesizer implements IReusableStackSynthesizer { + /** + * Default ARN qualifier + */ + public static readonly DEFAULT_QUALIFIER = 'hnb659fds'; + + /** + * Default CloudFormation role ARN. + */ + public static readonly DEFAULT_CLOUDFORMATION_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region}'; + + /** + * Default deploy role ARN. + */ + public static readonly DEFAULT_DEPLOY_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region}'; + + /** + * Default lookup role ARN for missing values. + */ + public static readonly DEFAULT_LOOKUP_ROLE_ARN = 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region}'; + + /** + * Use the Default Staging Resources, creating a single stack per environment this app is deployed in + */ + public static defaultResources(options: DefaultResourcesOptions) { + validateNoTokens(options, 'AppStagingSynthesizer'); + + return AppStagingSynthesizer.customFactory({ + factory: DefaultStagingStack.factory(options), + deploymentIdentities: options.deploymentIdentities, + bootstrapQualifier: options.bootstrapQualifier, + oncePerEnv: true, + }); + } + + /** + * Use these exact staging resources for every stack that this synthesizer is used for + */ + public static customResources(options: CustomResourcesOptions) { + return AppStagingSynthesizer.customFactory({ + deploymentIdentities: options.deploymentIdentities, + bootstrapQualifier: options.bootstrapQualifier, + oncePerEnv: false, + factory: { + obtainStagingResources() { + return options.resources; + }, + }, + }); + } + + /** + * Supply your own stagingStackFactory method for creating an IStagingStack when + * a stack is bound to the synthesizer. + * + * By default, `oncePerEnv = true`, which means that a new instance of the IStagingStack + * will be created in new environments. Set `oncePerEnv = false` to turn off that behavior. + */ + public static customFactory(options: CustomFactoryOptions) { + const oncePerEnv = options.oncePerEnv ?? true; + const factory = oncePerEnv ? new PerEnvironmentStagingFactory(options.factory) : options.factory; + + return new AppStagingSynthesizer({ + factory, + bootstrapQualifier: options.bootstrapQualifier, + deploymentIdentities: options.deploymentIdentities, + }); + } + + private readonly roles: Required; + + private constructor(private readonly props: AppStagingSynthesizerProps) { + super(); + + this.roles = { + deploymentRole: props.deploymentIdentities?.roles.deploymentRole ?? + BootstrapRole.fromRoleArn(AppStagingSynthesizer.DEFAULT_DEPLOY_ROLE_ARN), + cloudFormationExecutionRole: props.deploymentIdentities?.roles.cloudFormationExecutionRole ?? + BootstrapRole.fromRoleArn(AppStagingSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN), + lookupRole: this.props.deploymentIdentities?.roles.lookupRole ?? + BootstrapRole.fromRoleArn(AppStagingSynthesizer.DEFAULT_LOOKUP_ROLE_ARN), + }; + } + + /** + * Returns a version of the synthesizer bound to a stack. + */ + public reusableBind(stack: Stack): IBoundAppStagingSynthesizer { + this.checkEnvironmentGnosticism(stack); + const qualifier = this.props.bootstrapQualifier ?? + stack.node.tryGetContext(BOOTSTRAP_QUALIFIER_CONTEXT) ?? + AppStagingSynthesizer.DEFAULT_QUALIFIER; + const spec = new StringSpecializer(stack, qualifier); + + const deployRole = this.roles.deploymentRole._specialize(spec); + + const context: ObtainStagingResourcesContext = { + environmentString: [ + Token.isUnresolved(stack.account) ? 'ACCOUNT' : stack.account, + Token.isUnresolved(stack.region) ? 'REGION' : stack.region, + ].join('-'), + deployRoleArn: deployRole._arnForCloudFormation(), + qualifier, + }; + + return new BoundAppStagingSynthesizer(stack, { + stagingResources: this.props.factory.obtainStagingResources(stack, context), + deployRole, + cloudFormationExecutionRole: this.roles.cloudFormationExecutionRole._specialize(spec), + lookupRole: this.roles.lookupRole._specialize(spec), + qualifier, + }); + } + + /** + * Implemented for legacy purposes; this will never be called. + */ + public bind(_stack: Stack) { + throw new Error('This is a legacy API, call reusableBind instead'); + } + + /** + * Implemented for legacy purposes; this will never be called. + */ + public synthesize(_session: ISynthesisSession): void { + throw new Error('This is a legacy API, call reusableBind instead'); + } + + /** + * Implemented for legacy purposes; this will never be called. + */ + public addFileAsset(_asset: FileAssetSource): FileAssetLocation { + throw new Error('This is a legacy API, call reusableBind instead'); + } + + /** + * Implemented for legacy purposes; this will never be called. + */ + public addDockerImageAsset(_asset: DockerImageAssetSource): DockerImageAssetLocation { + throw new Error('This is a legacy API, call reusableBind instead'); + } + + /** + * Check that we're only being used for exclusively gnostic or agnostic stacks. + * + * We can think about whether to loosen this requirement later. + */ + private checkEnvironmentGnosticism(stack: Stack) { + const isAgnostic = Token.isUnresolved(stack.account) || Token.isUnresolved(stack.region); + const agnosticStacks = AGNOSTIC_STACKS.for(stack); + const envAwareStacks = ENV_AWARE_STACKS.for(stack); + + (isAgnostic ? agnosticStacks : envAwareStacks).add(stack); + if (agnosticStacks.size > 0 && envAwareStacks.size > 0) { + + const describeStacks = (xs: Set) => Array.from(xs).map(s => s.node.path).join(', '); + + throw new Error([ + 'It is not safe to use AppStagingSynthesizer for both environment-agnostic and environment-aware stacks at the same time.', + 'Please either specify environments for all stacks or no stacks in the CDK App.', + `Stacks with environment: ${describeStacks(agnosticStacks)}.`, + `Stacks without environment: ${describeStacks(envAwareStacks)}.`, + ].join(' ')); + } + } +} + +/** + * Internal properties for BoundAppStagingSynthesizer + */ +interface BoundAppStagingSynthesizerProps { + /** + * The bootstrap qualifier + */ + readonly qualifier: string; + + /** + * The resources we end up using for this synthesizer + */ + readonly stagingResources: IStagingResources; + + /** + * The deploy role + */ + readonly deployRole: BootstrapRole; + + /** + * CloudFormation Execution Role + */ + readonly cloudFormationExecutionRole: BootstrapRole; + + /** + * Lookup Role + */ + readonly lookupRole: BootstrapRole; +} + +class BoundAppStagingSynthesizer extends StackSynthesizer implements IBoundAppStagingSynthesizer { + private readonly stagingStack: IStagingResources; + private readonly assetManifest = new AssetManifestBuilder(); + private readonly qualifier: string; + private readonly dependencyStacks: Set = new Set(); + + constructor(stack: Stack, private readonly props: BoundAppStagingSynthesizerProps) { + super(); + super.bind(stack); + + this.qualifier = props.qualifier; + this.stagingStack = props.stagingResources; + } + /** + * The qualifier used to bootstrap this stack + */ + public get bootstrapQualifier(): string | undefined { + // Not sure why we need this. + return this.qualifier; + } + + public synthesize(session: ISynthesisSession): void { + const templateAssetSource = this.synthesizeTemplate(session, this.props.lookupRole?._arnForCloudAssembly()); + const templateAsset = this.addFileAsset(templateAssetSource); + + const dependencies = Array.from(this.dependencyStacks).flatMap((d) => d.artifactId); + const assetManifestId = this.assetManifest.emitManifest(this.boundStack, session, {}, dependencies); + + const lookupRoleArn = this.props.lookupRole?._arnForCloudAssembly(); + + this.emitArtifact(session, { + assumeRoleArn: this.props.deployRole?._arnForCloudAssembly(), + additionalDependencies: [assetManifestId], + stackTemplateAssetObjectUrl: templateAsset.s3ObjectUrlWithPlaceholders, + cloudFormationExecutionRoleArn: this.props.cloudFormationExecutionRole?._arnForCloudAssembly(), + lookupRole: lookupRoleArn ? { arn: lookupRoleArn } : undefined, + }); + } + + /** + * Add a file asset to the manifest. + */ + public addFileAsset(asset: FileAssetSource): FileAssetLocation { + const { bucketName, assumeRoleArn, prefix, dependencyStack } = this.stagingStack.addFile(asset); + const location = this.assetManifest.defaultAddFileAsset(this.boundStack, asset, { + bucketName: translateCfnTokenToAssetToken(bucketName), + bucketPrefix: prefix, + role: assumeRoleArn ? { assumeRoleArn: translateCfnTokenToAssetToken(assumeRoleArn) } : undefined, + }); + + if (dependencyStack) { + this.boundStack.addDependency(dependencyStack, 'stack depends on the staging stack for staging resources'); + this.dependencyStacks.add(dependencyStack); + } + + return this.cloudFormationLocationFromFileAsset(location); + } + + /** + * Add a docker image asset to the manifest. + */ + public addDockerImageAsset(asset: DockerImageAssetSource): DockerImageAssetLocation { + const { repoName, assumeRoleArn, dependencyStack } = this.stagingStack.addDockerImage(asset); + const location = this.assetManifest.defaultAddDockerImageAsset(this.boundStack, asset, { + repositoryName: translateCfnTokenToAssetToken(repoName), + role: assumeRoleArn ? { assumeRoleArn: translateCfnTokenToAssetToken(assumeRoleArn) } : undefined, + }); + + if (dependencyStack) { + this.boundStack.addDependency(dependencyStack, 'stack depends on the staging stack for staging resources'); + this.dependencyStacks.add(dependencyStack); + } + + return this.cloudFormationLocationFromDockerImageAsset(location); + } +} diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts new file mode 100644 index 0000000000000..a5454ca51d021 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts @@ -0,0 +1,130 @@ +import { StringSpecializer, translateAssetTokenToCfnToken, translateCfnTokenToAssetToken } from 'aws-cdk-lib/core/lib/helpers-internal'; + +/** + * Bootstrapped role specifier. These roles must exist already. + * This class does not create new IAM Roles. + */ +export class BootstrapRole { + /** + * Use the currently assumed role/credentials + */ + public static cliCredentials() { + return new BootstrapRole(BootstrapRole.CLI_CREDS); + } + + /** + * Specify an existing IAM Role to assume + */ + public static fromRoleArn(arn: string) { + StringSpecializer.validateNoTokens(arn, 'BootstrapRole ARN'); + return new BootstrapRole(arn); + } + + private static CLI_CREDS = 'cli-credentials'; + + private constructor(private readonly roleArn: string) {} + + /** + * Whether or not this is object was created using BootstrapRole.cliCredentials() + */ + public isCliCredentials() { + return this.roleArn === BootstrapRole.CLI_CREDS; + } + + /** + * @internal + */ + public _arnForCloudFormation() { + return this.isCliCredentials() ? undefined : translateAssetTokenToCfnToken(this.roleArn); + } + + /** + * @internal + */ + public _arnForCloudAssembly() { + return this.isCliCredentials() ? undefined : translateCfnTokenToAssetToken(this.roleArn); + } + + /** + * @internal + */ + public _specialize(spec: StringSpecializer) { + return new BootstrapRole(spec.specialize(this.roleArn)); + } +} + +/** + * Deployment identities are the class of roles to be assumed by the CDK + * when deploying the App. + */ +export class DeploymentIdentities { + /** + * Use CLI credentials for all deployment identities. + */ + public static cliCredentials(): DeploymentIdentities { + return new DeploymentIdentities({ + cloudFormationExecutionRole: BootstrapRole.cliCredentials(), + deploymentRole: BootstrapRole.cliCredentials(), + lookupRole: BootstrapRole.cliCredentials(), + }); + } + + /** + * Specify your own roles for all deployment identities. These roles + * must already exist. + */ + public static specifyRoles(roles: BootstrapRoles): DeploymentIdentities { + return new DeploymentIdentities(roles); + } + + private constructor( + /** roles that are bootstrapped to your account. */ + public readonly roles: BootstrapRoles, + ) {} +} + +/** + * Roles that are bootstrapped to your account. + */ +export interface BootstrapRoles { + /** + * CloudFormation Execution Role + * + * @default - use bootstrapped role + */ + readonly cloudFormationExecutionRole?: BootstrapRole; + + /** + * Deployment Action Role + * + * @default - use boostrapped role + */ + readonly deploymentRole?: BootstrapRole; + + /** + * Lookup Role + * + * @default - use bootstrapped role + */ + readonly lookupRole?: BootstrapRole; +} + +/** + * Roles that are included in the Staging Stack + * (for access to Staging Resources) + */ +export interface StagingRoles { + /** + * File Asset Publishing Role + * + * @default - staging stack creates a file asset publishing role + */ + readonly fileAssetPublishingRole?: BootstrapRole; + + /** + * Docker Asset Publishing Role + * + * @default - staging stack creates a docker asset publishing role + */ + readonly dockerAssetPublishingRole?: BootstrapRole; +} diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts new file mode 100644 index 0000000000000..468323e0ec2c4 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts @@ -0,0 +1,425 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { + App, + ArnFormat, + BootstraplessSynthesizer, + DockerImageAssetSource, + Duration, + FileAssetSource, + ISynthesisSession, + RemovalPolicy, + Stack, + StackProps, +} from 'aws-cdk-lib'; +import * as ecr from 'aws-cdk-lib/aws-ecr'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as kms from 'aws-cdk-lib/aws-kms'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import { StringSpecializer } from 'aws-cdk-lib/core/lib/helpers-internal'; +import { BootstrapRole } from './bootstrap-roles'; +import { FileStagingLocation, IStagingResources, IStagingResourcesFactory, ImageStagingLocation } from './staging-stack'; + +export const DEPLOY_TIME_PREFIX = 'deploy-time/'; + +/** + * User configurable options to the DefaultStagingStack. + */ +export interface DefaultStagingStackOptions { + /** + * A unique identifier for the application that the staging stack belongs to. + * + * This identifier will be used in the name of staging resources + * created for this application, and should be unique across CDK apps. + * + * The identifier should include lowercase characters and dashes ('-') only + * and have a maximum of 20 characters. + */ + readonly appId: string; + + /** + * Explicit name for the staging bucket + * + * @default - a well-known name unique to this app/env. + */ + readonly stagingBucketName?: string; + + /** + * Pass in an existing role to be used as the file publishing role. + * + * @default - a new role will be created + */ + readonly fileAssetPublishingRole?: BootstrapRole; + + /** + * Pass in an existing role to be used as the image publishing role. + * + * @default - a new role will be created + */ + readonly imageAssetPublishingRole?: BootstrapRole; + + /** + * The lifetime for deploy time file assets. + * + * Assets that are only necessary at deployment time (for instance, + * CloudFormation templates and Lambda source code bundles) will be + * automatically deleted after this many days. Assets that may be + * read from the staging bucket during your application's run time + * will not be deleted. + * + * Set this to the length of time you wish to be able to roll back to + * previous versions of your application without having to do a new + * `cdk synth` and re-upload of assets. + * + * @default - Duration.days(30) + */ + readonly deployTimeFileAssetLifetime?: Duration; + + /** + * The maximum number of image versions to store in a repository. + * + * Previous versions of an image can be stored for rollback purposes. + * Once a repository has more than 3 image versions stored, the oldest + * version will be discarded. This allows for sensible garbage collection + * while maintaining a few previous versions for rollback scenarios. + * + * @default - up to 3 versions stored + */ + readonly imageAssetVersionCount?: number; +} + +/** + * Default Staging Stack Properties + */ +export interface DefaultStagingStackProps extends DefaultStagingStackOptions, StackProps { + /** + * The ARN of the deploy action role, if given + * + * This role will need permissions to read from to the staging resources. + * + * @default - The CLI credentials are assumed, no additional permissions are granted. + */ + readonly deployRoleArn?: string; + + /** + * The qualifier used to specialize strings + * + * Shouldn't be necessary but who knows what people might do. + */ + readonly qualifier: string; +} + +/** + * A default Staging Stack that implements IStagingResources. + */ +export class DefaultStagingStack extends Stack implements IStagingResources { + /** + * Return a factory that will create DefaultStagingStacks + */ + public static factory(options: DefaultStagingStackOptions): IStagingResourcesFactory { + const appId = options.appId.toLocaleLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 20); + return { + obtainStagingResources(stack, context) { + const app = App.of(stack); + if (!App.isApp(app)) { + throw new Error(`Stack ${stack.stackName} must be part of an App`); + } + + const stackId = `StagingStack-${appId}-${context.environmentString}`; + return new DefaultStagingStack(app, stackId, { + ...options, + + // Does not need to contain environment because stack names are unique inside an env anyway + stackName: `StagingStack-${appId}`, + env: { + account: stack.account, + region: stack.region, + }, + appId, + qualifier: context.qualifier, + deployRoleArn: context.deployRoleArn, + }); + }, + }; + } + + /** + * Default asset publishing role name for file (S3) assets. + */ + private get fileRoleName() { + // This role name can be a maximum of 64 letters. The reason why + // we slice the appId and not the entire name is because this.region + // can be a token and we don't want to accidentally cut it off. + return `cdk-${this.appId}-file-role-${this.region}`; + } + + /** + * Default asset publishing role name for docker (ECR) assets. + */ + private get imageRoleName() { + // This role name can be a maximum of 64 letters. The reason why + // we slice the appId and not the entire name is because this.region + // can be a token and we don't want to accidentally cut it off. + return `cdk-${this.appId}-image-role-${this.region}`; + } + + /** + * The app-scoped, evironment-keyed staging bucket. + */ + public readonly stagingBucket?: s3.Bucket; + + /** + * The app-scoped, environment-keyed ecr repositories associated with this app. + */ + public readonly stagingRepos: Record; + + /** + * The stack to add dependencies to. + */ + public readonly dependencyStack: Stack; + + private readonly appId: string; + private readonly stagingBucketName?: string; + + /** + * File publish role ARN in asset manifest format + */ + private readonly providedFileRole?: BootstrapRole; + private fileRole?: iam.IRole; + private fileRoleManifestArn?: string; + + /** + * Image publishing role ARN in asset manifest format + */ + private readonly providedImageRole?: BootstrapRole; + private imageRole?: iam.IRole; + private didImageRole = false; + private imageRoleManifestArn?: string; + + private readonly deployRoleArn?: string; + + constructor(scope: App, id: string, private readonly props: DefaultStagingStackProps) { + super(scope, id, { + ...props, + synthesizer: new BootstraplessSynthesizer(), + }); + + this.appId = this.validateAppId(props.appId); + this.dependencyStack = this; + + this.deployRoleArn = props.deployRoleArn; + this.stagingBucketName = props.stagingBucketName; + const specializer = new StringSpecializer(this, props.qualifier); + + this.providedFileRole = props.fileAssetPublishingRole?._specialize(specializer); + this.providedImageRole = props.imageAssetPublishingRole?._specialize(specializer); + this.stagingRepos = {}; + } + + private validateAppId(id: string) { + const errors = []; + if (id.length > 20) { + errors.push(`appId expected no more than 20 characters but got ${id.length} characters.`); + } + if (id !== id.toLocaleLowerCase()) { + errors.push('appId only accepts lowercase characters.'); + } + if (!/^[a-z0-9-]*$/.test(id)) { + errors.push('appId expects only letters, numbers, and dashes (\'-\')'); + } + + if (errors.length > 0) { + throw new Error([ + `appId ${id} has errors:`, + ...errors, + ].join('\n')); + } + return id; + } + + private ensureFileRole() { + if (this.providedFileRole) { + // Override + this.fileRoleManifestArn = this.providedFileRole._arnForCloudAssembly(); + const cfnArn = this.providedFileRole._arnForCloudFormation(); + this.fileRole = cfnArn ? iam.Role.fromRoleArn(this, 'CdkFileRole', cfnArn) : undefined; + return; + } + + const roleName = this.fileRoleName; + this.fileRole = new iam.Role(this, 'CdkFileRole', { + roleName, + assumedBy: new iam.AccountPrincipal(this.account), + }); + + this.fileRoleManifestArn = Stack.of(this).formatArn({ + partition: '${AWS::Partition}', + region: '', // iam is global + service: 'iam', + resource: 'role', + resourceName: roleName, + arnFormat: ArnFormat.SLASH_RESOURCE_NAME, + }); + } + + private ensureImageRole() { + // It may end up setting imageRole to undefined, but at least we tried + if (this.didImageRole) { + return; + } + this.didImageRole = true; + + if (this.providedImageRole) { + // Override + this.imageRoleManifestArn = this.providedImageRole._arnForCloudAssembly(); + const cfnArn = this.providedImageRole._arnForCloudFormation(); + this.imageRole = cfnArn ? iam.Role.fromRoleArn(this, 'CdkImageRole', cfnArn) : undefined; + return; + } + + const roleName = this.imageRoleName; + this.imageRole = new iam.Role(this, 'CdkImageRole', { + roleName, + assumedBy: new iam.AccountPrincipal(this.account), + }); + this.imageRoleManifestArn = Stack.of(this).formatArn({ + partition: '${AWS::Partition}', + region: '', // iam is global + service: 'iam', + resource: 'role', + resourceName: roleName, + arnFormat: ArnFormat.SLASH_RESOURCE_NAME, + }); + } + + private createBucketKey(): kms.IKey { + return new kms.Key(this, 'BucketKey', { + alias: `alias/cdk-${this.appId}-staging`, + admins: [new iam.AccountPrincipal(this.account)], + }); + } + + private getCreateBucket() { + const stagingBucketName = this.stagingBucketName ?? `cdk-${this.appId}-staging-${this.account}-${this.region}`; + const bucketId = 'CdkStagingBucket'; + const createdBucket = this.node.tryFindChild(bucketId) as s3.Bucket; + if (createdBucket) { + return stagingBucketName; + } + + this.ensureFileRole(); + const key = this.createBucketKey(); + + // Create the bucket once the dependencies have been created + const bucket = new s3.Bucket(this, bucketId, { + bucketName: stagingBucketName, + removalPolicy: RemovalPolicy.RETAIN, + encryption: s3.BucketEncryption.KMS, + encryptionKey: key, + + // Many AWS account safety checkers will complain when buckets aren't versioned + versioned: true, + // Many AWS account safety checkers will complain when SSL isn't enforced + enforceSSL: true, + }); + + if (this.fileRole) { + bucket.grantReadWrite(this.fileRole); + } + + if (this.deployRoleArn) { + bucket.addToResourcePolicy(new iam.PolicyStatement({ + actions: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + resources: [bucket.bucketArn, bucket.arnForObjects('*')], + principals: [new iam.ArnPrincipal(this.deployRoleArn)], + })); + } + + // Objects should never be overwritten, but let's make sure we have a lifecycle policy + // for it anyway. + bucket.addLifecycleRule({ + noncurrentVersionExpiration: Duration.days(365), + }); + + bucket.addLifecycleRule({ + prefix: DEPLOY_TIME_PREFIX, + expiration: this.props.deployTimeFileAssetLifetime ?? Duration.days(30), + }); + + return stagingBucketName; + } + + /** + * Returns the well-known name of the repo + */ + private getCreateRepo(asset: DockerImageAssetSource): string { + if (!asset.assetName) { + throw new Error('Assets synthesized with AppScopedStagingSynthesizer must include an \'assetName\' in the asset source definition.'); + } + + // Create image publishing role if it doesn't exist + this.ensureImageRole(); + + const repoName = generateRepoName(`${this.appId}/${asset.assetName}`); + if (this.stagingRepos[asset.assetName] === undefined) { + this.stagingRepos[asset.assetName] = new ecr.Repository(this, repoName, { + repositoryName: repoName, + lifecycleRules: [{ + description: 'Garbage collect old image versions and keep the specified number of latest versions', + maxImageCount: this.props.imageAssetVersionCount ?? 3, + }], + }); + if (this.imageRole) { + this.stagingRepos[asset.assetName].grantPullPush(this.imageRole); + this.stagingRepos[asset.assetName].grantRead(this.imageRole); + } + } + return repoName; + + function generateRepoName(name: string): string { + return name.toLocaleLowerCase().replace('.', '-'); + } + } + + public addFile(asset: FileAssetSource): FileStagingLocation { + // Has side effects so must go first + const bucketName = this.getCreateBucket(); + + return { + bucketName, + assumeRoleArn: this.fileRoleManifestArn, + prefix: asset.deployTime ? DEPLOY_TIME_PREFIX : undefined, + dependencyStack: this, + }; + } + + public addDockerImage(asset: DockerImageAssetSource): ImageStagingLocation { + // Has side effects so must go first + const repoName = this.getCreateRepo(asset); + + return { + repoName, + assumeRoleArn: this.imageRoleManifestArn, + dependencyStack: this, + }; + } + + /** + * Synthesizes the cloudformation template into a cloud assembly. + * @internal + */ + public _synthesizeTemplate(session: ISynthesisSession, lookupRoleArn?: string | undefined): void { + super._synthesizeTemplate(session, lookupRoleArn); + + const builder = session.assembly; + const outPath = path.join(builder.outdir, this.templateFile); + const size = fs.statSync(outPath).size; + if (size > 51200) { + throw new Error(`Staging resource template cannot be greater than 51200 bytes, but got ${size} bytes`); + } + } +} diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/index.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/index.ts new file mode 100644 index 0000000000000..2a5055670e09d --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/index.ts @@ -0,0 +1,4 @@ +export * from './default-staging-stack'; +export * from './app-staging-synthesizer'; +export * from './bootstrap-roles'; +export * from './staging-stack'; diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/per-env-staging-factory.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/per-env-staging-factory.ts new file mode 100644 index 0000000000000..3d3c8fd5c50b2 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/per-env-staging-factory.ts @@ -0,0 +1,32 @@ +import { Stack } from 'aws-cdk-lib'; +import { AppScopedGlobal } from './private/app-global'; +import { IStagingResources, IStagingResourcesFactory, ObtainStagingResourcesContext } from './staging-stack'; + +/** + * Per-environment cache + * + * This is a global because we might have multiple instances of this class + * in the app, but we want to cache across all of them. + */ +const ENVIRONMENT_CACHE = new AppScopedGlobal(() => new Map()); + +/** + * Wraps another IStagingResources factory, and caches the result on a per-environment basis. + */ +export class PerEnvironmentStagingFactory implements IStagingResourcesFactory { + constructor(private readonly wrapped: IStagingResourcesFactory) { } + + public obtainStagingResources(stack: Stack, context: ObtainStagingResourcesContext): IStagingResources { + const cacheKey = context.environmentString; + + const cache = ENVIRONMENT_CACHE.for(stack); + const existing = cache.get(cacheKey); + if (existing) { + return existing; + } + + const result = this.wrapped.obtainStagingResources(stack, context); + cache.set(cacheKey, result); + return result; + } +} diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/private/app-global.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/private/app-global.ts new file mode 100644 index 0000000000000..2cd36d0264a5b --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/private/app-global.ts @@ -0,0 +1,33 @@ +import { App } from 'aws-cdk-lib'; +import { IConstruct } from 'constructs'; + +/** + * Hold an App-wide global variable + * + * This is a replacement for a `static` variable, but does the right thing in case people + * instantiate multiple Apps in the same process space (for example, in unit tests or + * people using `cli-lib` in advanced configurations). + * + * This class assumes that the global you're going to be storing is a mutable object. + */ +export class AppScopedGlobal { + private readonly map = new WeakMap(); + + constructor(private readonly factory: () => A) { + } + + public for(ctr: IConstruct): A { + const app = App.of(ctr); + if (!App.isApp(app)) { + throw new Error(`Construct ${ctr.node.path} must be part of an App`); + } + + const existing = this.map.get(app); + if (existing) { + return existing; + } + const instance = this.factory(); + this.map.set(app, instance); + return instance; + } +} diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/private/no-tokens.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/private/no-tokens.ts new file mode 100644 index 0000000000000..befb88fc8f4de --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/private/no-tokens.ts @@ -0,0 +1,9 @@ +import { StringSpecializer } from 'aws-cdk-lib/core/lib/helpers-internal'; + +export function validateNoTokens(props: A, context: string) { + for (const [key, value] of Object.entries(props)) { + if (typeof value === 'string') { + StringSpecializer.validateNoTokens(value, `${context} property '${key}'`); + } + } +} diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/staging-stack.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/staging-stack.ts new file mode 100644 index 0000000000000..9cc24dbb3bce5 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/lib/staging-stack.ts @@ -0,0 +1,120 @@ +import { DockerImageAssetSource, FileAssetSource, Stack } from 'aws-cdk-lib'; +import { IConstruct } from 'constructs'; + +/** + * Information returned by the Staging Stack for each file asset. + */ +export interface FileStagingLocation { + /** + * The name of the staging bucket + */ + readonly bucketName: string; + + /** + * A prefix to add to the keys + * + * @default '' + */ + readonly prefix?: string; + + /** + * The ARN to assume to write files to this bucket + * + * @default - Don't assume a role + */ + readonly assumeRoleArn?: string; + + /** + * The stack that creates this bucket (leads to dependencies on it) + * + * @default - Don't add dependencies + */ + readonly dependencyStack?: Stack; +} + +/** + * Information returned by the Staging Stack for each image asset + */ +export interface ImageStagingLocation { + /** + * The name of the staging repository + */ + readonly repoName: string; + + /** + * The arn to assume to write files to this repository + * + * @default - Don't assume a role + */ + readonly assumeRoleArn?: string; + + /** + * The stack that creates this repository (leads to dependencies on it) + * + * @default - Don't add dependencies + */ + readonly dependencyStack?: Stack; +} + +/** + * Staging Resource interface. + */ +export interface IStagingResources extends IConstruct { + /** + * Return staging resource information for a file asset. + */ + addFile(asset: FileAssetSource): FileStagingLocation; + + /** + * Return staging resource information for a docker asset. + */ + addDockerImage(asset: DockerImageAssetSource): ImageStagingLocation; +} + +/** + * Staging Resource Factory interface. + * + * The function included in this class will be called by the synthesizer + * to create or reference an IStagingResources construct that has the necessary + * staging resources for the stack. + */ +export interface IStagingResourcesFactory { + /** + * Return an object that will manage staging resources for the given stack + * + * This is called whenever the the `AppStagingSynthesizer` binds to a specific + * stack, and allows selecting where the staging resources go. + * + * This method can choose to either create a new construct (perhaps a stack) + * and return it, or reference an existing construct. + * + * @param stack - stack to return an appropriate IStagingStack for + */ + obtainStagingResources(stack: Stack, context: ObtainStagingResourcesContext): IStagingResources; +} + +/** + * Context parameters for the 'obtainStagingResources' function + */ +export interface ObtainStagingResourcesContext { + /** + * A unique string describing the environment that is guaranteed not to have tokens in it + */ + readonly environmentString: string; + + /** + * The ARN of the deploy action role, if given + * + * This role will need permissions to read from to the staging resources. + * + * @default - Deploy role ARN is unknown + */ + readonly deployRoleArn?: string; + + /** + * The qualifier passed to the synthesizer + * + * The staging stack shouldn't need this, but it might. + */ + readonly qualifier: string; +} diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/package.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/package.json new file mode 100644 index 0000000000000..7b5e5e9ef5dee --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/package.json @@ -0,0 +1,101 @@ +{ + "name": "@aws-cdk/app-staging-synthesizer-alpha", + "private": true, + "version": "0.0.0", + "description": "Cdk synthesizer for with app-scoped staging stack", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "projectReferences": true, + "metadata": { + "jsii": { + "rosetta": { + "strict": true + } + } + }, + "targets": { + "java": { + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "cdk-app-staging-synthesizer-alpha" + }, + "package": "software.amazon.awscdk.app.staging.synthesizer.alpha" + }, + "python": { + "distName": "aws-cdk.app-staging-synthesizer-alpha", + "module": "aws_cdk.app_staging_synthesizer_alpha", + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 2" + ] + }, + "dotnet": { + "namespace": "Amazon.CDK.App.Staging.Synthesizer.Alpha", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/main/logo/default-256-dark.png" + } + } + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/app-staging-synthesizer-alpha" + }, + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "integ": "integ-runner", + "pkglint": "pkglint -f", + "package": "cdk-package", + "awslint": "cdk-awslint", + "build+test": "yarn build && yarn test", + "build+test+package": "yarn build+test && yarn package", + "compat": "cdk-compat", + "rosetta:extract": "yarn --silent jsii-rosetta extract", + "build+extract": "yarn build && yarn rosetta:extract", + "build+test+extract": "yarn build+test && yarn rosetta:extract" + }, + "keywords": [ + "aws", + "cdk" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "homepage": "https://github.com/aws/aws-cdk", + "engines": { + "node": ">= 14.15.0" + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + }, + "cdk-build": { + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "dependencies": { + "aws-cdk-lib": "0.0.0", + "constructs": "^10.0.0" + }, + "devDependencies": { + "aws-cdk-lib": "0.0.0", + "@aws-cdk/integ-runner": "0.0.0", + "@aws-cdk/integ-tests-alpha": "0.0.0", + "constructs": "^10.0.0", + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/pkglint": "0.0.0" + }, + "peerDependencies": { + "aws-cdk-lib": "0.0.0", + "constructs": "^10.0.0" + } +} diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/rosetta/default.ts-fixture b/packages/@aws-cdk/app-staging-synthesizer-alpha/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..150cd4c706021 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/rosetta/default.ts-fixture @@ -0,0 +1,22 @@ +// Fixture with packages imported, but nothing else +import { App, Stack, StackProps, Duration, DockerImageAssetSource, FileAssetSource } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { + AppStagingSynthesizer, + BootstrapRole, + DefaultStagingStack, + DefaultStagingStackOptions, + IStagingResources, + FileStagingLocation, + ImageStagingLocation, + DeploymentIdentities, +} from '@aws-cdk/app-staging-synthesizer-alpha'; +import * as path from 'path'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/rosetta/with-custom-staging.ts-fixture b/packages/@aws-cdk/app-staging-synthesizer-alpha/rosetta/with-custom-staging.ts-fixture new file mode 100644 index 0000000000000..981f76ecbbac1 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/rosetta/with-custom-staging.ts-fixture @@ -0,0 +1,44 @@ +// Fixture with packages imported, but nothing else +import { App, Stack, StackProps, DockerImageAssetSource, FileAssetSource } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { + AppStagingSynthesizer, + DefaultStagingStack, + BootstrapRole, + FileStagingLocation, + ImageStagingLocation, + ObtainStagingResourcesContext, + IStagingResourcesFactory, + IStagingResources, +} from '@aws-cdk/app-staging-synthesizer-alpha'; + +interface CustomStagingStackProps extends StackProps {} + +class CustomStagingStack extends Stack implements IStagingResources { + public constructor(scope: Construct, id: string, props: CustomStagingStackProps) { + super(scope, id, props); + } + + public addFile(asset: FileAssetSource): FileStagingLocation { + return { + bucketName: 'myBucket', + assumeRoleArn: 'myArn', + dependencyStack: this, + }; + } + + public addDockerImage(asset: DockerImageAssetSource): ImageStagingLocation { + return { + repoName: 'myRepo', + assumeRoleArn: 'myArn', + dependencyStack: this, + }; + } +} + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + /// here + } +} diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts new file mode 100644 index 0000000000000..d6a47ba76c65e --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts @@ -0,0 +1,493 @@ +import * as fs from 'fs'; +import { App, Stack, CfnResource, FileAssetPackaging, Token, Lazy, Duration } from 'aws-cdk-lib'; +import { Match, Template } from 'aws-cdk-lib/assertions'; +import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema'; +import { CloudAssembly } from 'aws-cdk-lib/cx-api'; +import { evaluateCFN } from './evaluate-cfn'; +import { APP_ID, CFN_CONTEXT, isAssetManifest, last } from './util'; +import { AppStagingSynthesizer, DEPLOY_TIME_PREFIX } from '../lib'; + +describe(AppStagingSynthesizer, () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID }), + }); + stack = new Stack(app, 'Stack', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + }); + + test('stack template is in asset manifest', () => { + // GIVEN + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + }); + + // WHEN + const asm = app.synth(); + + // THEN -- the S3 url is advertised on the stack artifact + const stackArtifact = asm.getStackArtifact('Stack'); + + const templateObjectKey = `${DEPLOY_TIME_PREFIX}${last(stackArtifact.stackTemplateAssetObjectUrl?.split('/'))}`; + expect(stackArtifact.stackTemplateAssetObjectUrl).toEqual(`s3://cdk-${APP_ID}-staging-000000000000-us-east-1/${templateObjectKey}`); + + // THEN - the template is in the asset manifest + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + expect(manifestArtifact).toBeDefined(); + const manifest: cxschema.AssetManifest = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); + + const firstFile = (manifest.files ? manifest.files[Object.keys(manifest.files)[0]] : undefined) ?? {}; + + expect(firstFile).toEqual({ + source: { path: 'Stack.template.json', packaging: 'file' }, + destinations: { + '000000000000-us-east-1': { + bucketName: `cdk-${APP_ID}-staging-000000000000-us-east-1`, + objectKey: templateObjectKey, + region: 'us-east-1', + assumeRoleArn: `arn:\${AWS::Partition}:iam::000000000000:role/cdk-${APP_ID}-file-role-us-east-1`, + }, + }, + }); + }); + + test('stack template is in the asset manifest - environment tokens', () => { + const app2 = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID }), + }); + const accountToken = Token.asString('111111111111'); + const regionToken = Token.asString('us-east-2'); + const stack2 = new Stack(app2, 'Stack2', { + env: { + account: accountToken, + region: regionToken, + }, + }); + + // GIVEN + new CfnResource(stack2, 'Resource', { + type: 'Some::Resource', + }); + + // WHEN + const asm = app2.synth(); + + // THEN -- the S3 url is advertised on the stack artifact + const stackArtifact = asm.getStackArtifact('Stack2'); + + const templateObjectKey = `${DEPLOY_TIME_PREFIX}${last(stackArtifact.stackTemplateAssetObjectUrl?.split('/'))}`; + expect(stackArtifact.stackTemplateAssetObjectUrl).toEqual(`s3://cdk-${APP_ID}-staging-${accountToken}-${regionToken}/${templateObjectKey}`); + + // THEN - the template is in the asset manifest + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + expect(manifestArtifact).toBeDefined(); + const manifest: cxschema.AssetManifest = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); + + const firstFile = (manifest.files ? manifest.files[Object.keys(manifest.files)[0]] : undefined) ?? {}; + + expect(firstFile).toEqual({ + source: { path: 'Stack2.template.json', packaging: 'file' }, + destinations: { + '111111111111-us-east-2': { + bucketName: `cdk-${APP_ID}-staging-111111111111-us-east-2`, + objectKey: templateObjectKey, + region: 'us-east-2', + assumeRoleArn: `arn:\${AWS::Partition}:iam::111111111111:role/cdk-${APP_ID}-file-role-us-east-2`, + }, + }, + }); + }); + + test('stack depends on staging stack', () => { + // WHEN + stack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'abcdef', + }); + + // THEN - we have a stack dependency on the staging stack + expect(stack.dependencies.length).toEqual(1); + const depStack = stack.dependencies[0]; + expect(depStack.stackName).toEqual(`StagingStack-${APP_ID}`); + }); + + test('add file asset', () => { + // WHEN + const location = stack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'abcdef', + }); + + // THEN - we have a fixed asset location + expect(evalCFN(location.bucketName)).toEqual(`cdk-${APP_ID}-staging-000000000000-us-east-1`); + expect(evalCFN(location.httpUrl)).toEqual(`https://s3.us-east-1.domain.aws/cdk-${APP_ID}-staging-000000000000-us-east-1/abcdef.js`); + + // THEN - object key contains source hash somewhere + expect(location.objectKey.indexOf('abcdef')).toBeGreaterThan(-1); + }); + + test('file asset depends on staging stack', () => { + // WHEN + stack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'abcdef', + }); + + const asm = app.synth(); + + // THEN - the template is in the asset manifest + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + expect(manifestArtifact).toBeDefined(); + expect(manifestArtifact.manifest.dependencies).toEqual([`StagingStack-${APP_ID}-000000000000-us-east-1`]); + }); + + test('adding multiple files only creates one bucket', () => { + // WHEN + const location1 = stack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'abcdef', + }); + const location2 = stack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'zyxwvu', + }); + + // THEN - assets have the same location + expect(evalCFN(location1.bucketName)).toEqual(evalCFN(location2.bucketName)); + }); + + describe('deploy time assets', () => { + test('have the \'deploy-time/\' prefix', () => { + // WHEN + const location = stack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'abcdef', + deployTime: true, + }); + + // THEN - asset has bucket prefix + expect(evalCFN(location.objectKey)).toEqual(`${DEPLOY_TIME_PREFIX}abcdef.js`); + }); + + test('do not get specified bucketPrefix', () => { + // GIVEN + app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: APP_ID }), + }); + stack = new Stack(app, 'Stack', { + env: { + account: '000000000000', + region: 'us-west-2', + }, + }); + + // WHEN + const location = stack.synthesizer.addFileAsset({ + fileName: __filename, + packaging: FileAssetPackaging.FILE, + sourceHash: 'abcdef', + deployTime: true, + }); + + // THEN - asset has bucket prefix + expect(evalCFN(location.objectKey)).toEqual(`${DEPLOY_TIME_PREFIX}abcdef.js`); + }); + + test('have s3 bucket has lifecycle rule by default', () => { + // GIVEN + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + }); + + // WHEN + const asm = app.synth(); + + // THEN + Template.fromJSON(getStagingResourceStack(asm).template).hasResourceProperties('AWS::S3::Bucket', { + LifecycleConfiguration: { + Rules: Match.arrayWith([{ + ExpirationInDays: 30, + Prefix: DEPLOY_TIME_PREFIX, + Status: 'Enabled', + }]), + }, + }); + }); + + test('can have customized lifecycle rules', () => { + // GIVEN + app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: APP_ID, + deployTimeFileAssetLifetime: Duration.days(1), + }), + }); + stack = new Stack(app, 'Stack', { + env: { + account: '000000000000', + region: 'us-west-2', + }, + }); + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + }); + + // WHEN + const asm = app.synth(); + + // THEN + const stagingStackArtifact = asm.getStackArtifact(`StagingStack-${APP_ID}-000000000000-us-west-2`); + + Template.fromJSON(stagingStackArtifact.template).hasResourceProperties('AWS::S3::Bucket', { + LifecycleConfiguration: { + Rules: Match.arrayWith([{ + ExpirationInDays: 1, + Prefix: DEPLOY_TIME_PREFIX, + Status: 'Enabled', + }]), + }, + }); + }); + }); + + test('bucket has policy referring to deploymentrolearn', () => { + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + }); + + // WHEN + const asm = app.synth(); + + // THEN + const stagingStackArtifact = asm.getStackArtifact(`StagingStack-${APP_ID}-000000000000-us-east-1`); + + Template.fromJSON(stagingStackArtifact.template).hasResourceProperties('AWS::S3::BucketPolicy', { + PolicyDocument: { + Statement: Match.arrayWith([ + Match.objectLike({ + Effect: 'Allow', + Principal: { + AWS: Match.anyValue(), + }, + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + }), + ]), + }, + }); + }); + + test('add docker image asset', () => { + // WHEN + const assetName = 'abcdef'; + const location = stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + assetName, + }); + + // THEN - we have a fixed asset location + const repo = `${APP_ID}/${assetName}`; + expect(evalCFN(location.repositoryName)).toEqual(repo); + expect(evalCFN(location.imageUri)).toEqual(`000000000000.dkr.ecr.us-east-1.domain.aws/${repo}:abcdef`); + }); + + test('throws with docker image asset without assetName', () => { + expect(() => stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + })).toThrowError('Assets synthesized with AppScopedStagingSynthesizer must include an \'assetName\' in the asset source definition.'); + }); + + test('docker image asset depends on staging stack', () => { + // WHEN + const assetName = 'abcdef'; + stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + assetName, + }); + + const asm = app.synth(); + + // THEN - the template is in the asset manifest + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + expect(manifestArtifact).toBeDefined(); + expect(manifestArtifact.manifest.dependencies).toEqual([`StagingStack-${APP_ID}-000000000000-us-east-1`]); + }); + + test('docker image assets with different assetName have separate repos', () => { + // WHEN + const location1 = stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + assetName: 'firstAsset', + }); + + const location2 = stack.synthesizer.addDockerImageAsset({ + directoryName: './hello', + sourceHash: 'abcdef', + assetName: 'secondAsset', + }); + + // THEN - images have different asset locations + expect(evalCFN(location1.repositoryName)).not.toEqual(evalCFN(location2.repositoryName)); + }); + + test('docker image assets with same assetName live in same repos', () => { + // WHEN + const assetName = 'abcdef'; + const location1 = stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + assetName, + }); + + const location2 = stack.synthesizer.addDockerImageAsset({ + directoryName: './hello', + sourceHash: 'abcdefg', + assetName, + }); + + // THEN - images share same ecr repo + expect(evalCFN(location1.repositoryName)).toEqual(`${APP_ID}/${assetName}`); + expect(evalCFN(location1.repositoryName)).toEqual(evalCFN(location2.repositoryName)); + }); + + test('docker image repositories have lifecycle rule - default', () => { + // GIVEN + const assetName = 'abcdef'; + stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + assetName, + }); + + // WHEN + const asm = app.synth(); + + // THEN + Template.fromJSON(getStagingResourceStack(asm).template).hasResourceProperties('AWS::ECR::Repository', { + LifecyclePolicy: { + LifecyclePolicyText: Match.serializedJson({ + rules: Match.arrayWith([ + Match.objectLike({ + selection: Match.objectLike({ + countType: 'imageCountMoreThan', + countNumber: 3, + }), + }), + ]), + }), + }, + RepositoryName: `${APP_ID}/${assetName}`, + }); + }); + + test('docker image repositories have lifecycle rule - specified', () => { + // GIVEN + app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: APP_ID, + imageAssetVersionCount: 1, + }), + }); + stack = new Stack(app, 'Stack', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + + const assetName = 'abcdef'; + stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + assetName, + }); + + // WHEN + const asm = app.synth(); + + // THEN + Template.fromJSON(getStagingResourceStack(asm).template).hasResourceProperties('AWS::ECR::Repository', { + LifecyclePolicy: { + LifecyclePolicyText: Match.serializedJson({ + rules: Match.arrayWith([ + Match.objectLike({ + selection: Match.objectLike({ + countType: 'imageCountMoreThan', + countNumber: 1, + }), + }), + ]), + }), + }, + RepositoryName: `${APP_ID}/${assetName}`, + }); + }); + + describe('environment specifics', () => { + test('throws if App includes env-agnostic and specific env stacks', () => { + // GIVEN - App with Stack with specific environment + + // THEN - Expect environment agnostic stack to fail + expect(() => new Stack(app, 'NoEnvStack')).toThrowError(/It is not safe to use AppStagingSynthesizer/); + }); + }); + + test('throws if synthesizer props have tokens', () => { + expect(() => new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: Lazy.string({ produce: () => 'appId' }), + }), + })).toThrowError(/AppStagingSynthesizer property 'appId' may not contain tokens;/); + }); + + test('throws when staging resource stack is too large', () => { + // WHEN + const assetName = 'abcdef'; + for (let i = 0; i < 100; i++) { + stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + assetName: assetName + i, + }); + } + + // THEN + expect(() => app.synth()).toThrowError(/Staging resource template cannot be greater than 51200 bytes/); + }); + + /** + * Evaluate a possibly string-containing value the same way CFN would do + * + * (Be invariant to the specific Fn::Sub or Fn::Join we would output) + */ + function evalCFN(value: any) { + return evaluateCFN(stack.resolve(value), CFN_CONTEXT); + } + + /** + * Return the staging resource stack that is generated as part of the assembly + */ + function getStagingResourceStack(asm: CloudAssembly) { + return asm.getStackArtifact(`StagingStack-${APP_ID}-000000000000-us-east-1`); + } +}); diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/assets/Dockerfile b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/assets/Dockerfile new file mode 100644 index 0000000000000..4a015204a5983 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/assets/Dockerfile @@ -0,0 +1,2 @@ +FROM public.ecr.aws/lambda/python:3.10 +CMD echo hello world \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/assets/index.py b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/assets/index.py new file mode 100644 index 0000000000000..ed0f110e2e61e --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/assets/index.py @@ -0,0 +1 @@ +print('hello') \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts new file mode 100644 index 0000000000000..a46e1807f8c97 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts @@ -0,0 +1,189 @@ +import * as fs from 'fs'; +import { App, Stack, CfnResource } from 'aws-cdk-lib'; +import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema'; +import { APP_ID, isAssetManifest } from './util'; +import { AppStagingSynthesizer, BootstrapRole, DeploymentIdentities } from '../lib'; + +const CLOUDFORMATION_EXECUTION_ROLE = 'cloudformation-execution-role'; +const DEPLOY_ACTION_ROLE = 'deploy-action-role'; +const LOOKUP_ROLE = 'lookup-role'; + +describe('Boostrap Roles', () => { + test('default bootstrap role name is always under 64 characters', () => { + // GIVEN + const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: 'super long app id that needs to be cut', + }), + }); + const stack = new Stack(app, 'Stack', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + }); + + // WHEN + const asm = app.synth(); + + // THEN + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + expect(manifestArtifact).toBeDefined(); + const manifest: cxschema.AssetManifest = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); + const firstFile: any = (manifest.files ? manifest.files[Object.keys(manifest.files)[0]] : undefined) ?? {}; + expect(firstFile.destinations['000000000000-us-east-1'].assumeRoleArn).toEqual('arn:${AWS::Partition}:iam::000000000000:role/cdk-super-long-app-id-th-file-role-us-east-1'); + }); + + test('can supply existing arns for bootstrapped roles', () => { + // GIVEN + const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: APP_ID, + deploymentIdentities: DeploymentIdentities.specifyRoles({ + cloudFormationExecutionRole: BootstrapRole.fromRoleArn(CLOUDFORMATION_EXECUTION_ROLE), + lookupRole: BootstrapRole.fromRoleArn(LOOKUP_ROLE), + deploymentRole: BootstrapRole.fromRoleArn(DEPLOY_ACTION_ROLE), + }), + }), + }); + const stack = new Stack(app, 'Stack', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + }); + + // WHEN + const asm = app.synth(); + + // THEN + const stackArtifact = asm.getStackArtifact('Stack'); + + // Bootstrapped roles are as advertised + expect(stackArtifact.cloudFormationExecutionRoleArn).toEqual(CLOUDFORMATION_EXECUTION_ROLE); + expect(stackArtifact.lookupRole).toEqual({ arn: LOOKUP_ROLE }); + expect(stackArtifact.assumeRoleArn).toEqual(DEPLOY_ACTION_ROLE); + }); + + test('can supply existing arn for bucket staging role', () => { + // GIVEN + const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: APP_ID, + fileAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/S3Access'), + }), + }); + const stack = new Stack(app, 'Stack', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + }); + + // WHEN + const asm = app.synth(); + + // THEN + // Staging role is as advertised + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + expect(manifestArtifact).toBeDefined(); + const manifest: cxschema.AssetManifest = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); + const firstFile: any = (manifest.files ? manifest.files[Object.keys(manifest.files)[0]] : undefined) ?? {}; + expect(firstFile.destinations['000000000000-us-east-1'].assumeRoleArn).toEqual('arn:aws:iam::123456789012:role/S3Access'); + }); + + test('can provide existing arn for image staging role', () => { + // GIVEN + const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: APP_ID, + imageAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/ECRAccess'), + }), + }); + const stack = new Stack(app, 'Stack', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + stack.synthesizer.addDockerImageAsset({ + directoryName: '.', + sourceHash: 'abcdef', + assetName: 'myDockerAsset', + }); + + // WHEN + const asm = app.synth(); + + // THEN + // Image role is as advertised + const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; + expect(manifestArtifact).toBeDefined(); + const manifest: cxschema.AssetManifest = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); + const firstFile: any = (manifest.dockerImages ? manifest.dockerImages[Object.keys(manifest.dockerImages)[0]] : undefined) ?? {}; + expect(firstFile.destinations['000000000000-us-east-1'].assumeRoleArn).toEqual('arn:aws:iam::123456789012:role/ECRAccess'); + }); + + test('bootstrap roles can be specified as current cli credentials instead', () => { + // GIVEN + const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: APP_ID, + deploymentIdentities: DeploymentIdentities.cliCredentials(), + }), + }); + const stack = new Stack(app, 'Stack', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + new CfnResource(stack, 'Resource', { + type: 'Some::Resource', + }); + + // WHEN + const asm = app.synth(); + + // THEN + const stackArtifact = asm.getStackArtifact('Stack'); + + // Bootstrapped roles are undefined, which means current credentials are used + expect(stackArtifact.cloudFormationExecutionRoleArn).toBeUndefined(); + expect(stackArtifact.lookupRole).toBeUndefined(); + expect(stackArtifact.assumeRoleArn).toBeUndefined(); + }); + + test('qualifier is resolved in the synthesizer', () => { + const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + bootstrapQualifier: 'abcdef', + appId: APP_ID, + }), + }); + new Stack(app, 'Stack', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + + // WHEN + const asm = app.synth(); + + // THEN + const stackArtifact = asm.getStackArtifact('Stack'); + + // Bootstrapped role's asset manifest tokens are resolved, where possible + expect(stackArtifact.cloudFormationExecutionRoleArn).toEqual('arn:${AWS::Partition}:iam::000000000000:role/cdk-abcdef-cfn-exec-role-000000000000-us-east-1'); + }); +}); diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/default-staging-stack.test.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/default-staging-stack.test.ts new file mode 100644 index 0000000000000..d711195d4ca25 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/default-staging-stack.test.ts @@ -0,0 +1,44 @@ +import { App } from 'aws-cdk-lib'; +import { DefaultStagingStack } from '../lib'; + +describe('default staging stack', () => { + describe('appId fails', () => { + test('when appId > 20 characters', () => { + const app = new App(); + expect(() => new DefaultStagingStack(app, 'stack', { + appId: 'a'.repeat(21), + qualifier: 'qualifier', + })).toThrowError(/appId expected no more than 20 characters but got 21 characters./); + }); + + test('when uppercase characters are used', () => { + const app = new App(); + expect(() => new DefaultStagingStack(app, 'stack', { + appId: 'ABCDEF', + qualifier: 'qualifier', + })).toThrowError(/appId only accepts lowercase characters./); + }); + + test('when symbols are used', () => { + const app = new App(); + expect(() => new DefaultStagingStack(app, 'stack', { + appId: 'ca$h', + qualifier: 'qualifier', + })).toThrowError(/appId expects only letters, numbers, and dashes \('-'\)/); + }); + + test('when multiple rules broken at once', () => { + const app = new App(); + const appId = 'AB&C'.repeat(10); + expect(() => new DefaultStagingStack(app, 'stack', { + appId, + qualifier: 'qualifier', + })).toThrowError([ + `appId ${appId} has errors:`, + 'appId expected no more than 20 characters but got 40 characters.', + 'appId only accepts lowercase characters.', + 'appId expects only letters, numbers, and dashes (\'-\')', + ].join('\n')); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/evaluate-cfn.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/evaluate-cfn.ts new file mode 100644 index 0000000000000..917ffb6646195 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/evaluate-cfn.ts @@ -0,0 +1,114 @@ +/** + * Simple function to evaluate CloudFormation intrinsics. + * + * Note that this function is not production quality, it exists to support tests. + */ +export function evaluateCFN(object: any, context: {[key: string]: string} = {}): any { + const intrinsicFns: any = { + 'Fn::Join'(separator: string, args: string[]) { + if (typeof separator !== 'string') { + // CFN does not support expressions here! + throw new Error('\'separator\' argument of { Fn::Join } must be a string literal'); + } + return evaluate(args).map(evaluate).join(separator); + }, + + 'Fn::Split'(separator: string, args: any) { + if (typeof separator !== 'string') { + // CFN does not support expressions here! + throw new Error('\'separator\' argument of { Fn::Split } must be a string literal'); + } + return evaluate(args).split(separator); + }, + + 'Fn::Select'(index: number, args: any) { + return evaluate(args).map(evaluate)[index]; + }, + + 'Ref'(logicalId: string) { + if (!(logicalId in context)) { + throw new Error(`Trying to evaluate Ref of '${logicalId}' but not in context!`); + } + return context[logicalId]; + }, + + 'Fn::GetAtt'(logicalId: string, attributeName: string) { + const key = `${logicalId}.${attributeName}`; + if (!(key in context)) { + throw new Error(`Trying to evaluate Fn::GetAtt of '${logicalId}.${attributeName}' but not in context!`); + } + return context[key]; + }, + + 'Fn::Sub'(template: string, explicitPlaceholders?: Record) { + const placeholders = explicitPlaceholders ? evaluate(explicitPlaceholders) : context; + + if (typeof template !== 'string') { + throw new Error('The first argument to {Fn::Sub} must be a string literal (cannot be the result of an expression)'); + } + + return template.replace(/\$\{([a-zA-Z0-9.:-]*)\}/g, (_: string, key: string) => { + if (key in placeholders) { return placeholders[key]; } + throw new Error(`Unknown placeholder in Fn::Sub: ${key}`); + }); + }, + }; + + return evaluate(object); + + function evaluate(obj: any): any { + if (Array.isArray(obj)) { + return obj.map(evaluate); + } + + if (typeof obj === 'object') { + const intrinsic = parseIntrinsic(obj); + if (intrinsic) { + return evaluateIntrinsic(intrinsic); + } + + const ret: {[key: string]: any} = {}; + for (const key of Object.keys(obj)) { + ret[key] = evaluate(obj[key]); + } + return ret; + } + + return obj; + } + + function evaluateIntrinsic(intrinsic: Intrinsic) { + if (!(intrinsic.name in intrinsicFns)) { + throw new Error(`Intrinsic ${intrinsic.name} not supported here`); + } + + const argsAsArray = Array.isArray(intrinsic.args) ? intrinsic.args : [intrinsic.args]; + + return intrinsicFns[intrinsic.name].apply(intrinsicFns, argsAsArray); + } +} + +interface Intrinsic { + readonly name: string; + readonly args: any; +} + +function parseIntrinsic(x: any): Intrinsic | undefined { + if (typeof x !== 'object' || x === null) { return undefined; } + const keys = Object.keys(x); + if (keys.length === 1 && (isNameOfCloudFormationIntrinsic(keys[0]) || keys[0] === 'Ref')) { + return { + name: keys[0], + args: x[keys[0]], + }; + } + return undefined; +} + +function isNameOfCloudFormationIntrinsic(name: string): boolean { + if (!name.startsWith('Fn::')) { + return false; + } + // these are 'fake' intrinsics, only usable inside the parameter overrides of a CFN CodePipeline Action + return name !== 'Fn::GetArtifactAtt' && name !== 'Fn::GetParam'; +} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/StagingStack-default-resources-ACCOUNT-REGION.template.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/StagingStack-default-resources-ACCOUNT-REGION.template.json new file mode 100644 index 0000000000000..fae319dc47641 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/StagingStack-default-resources-ACCOUNT-REGION.template.json @@ -0,0 +1,472 @@ +{ + "Resources": { + "CdkFileRoleE26CEABA": { + "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" + }, + "RoleName": { + "Fn::Join": [ + "", + [ + "cdk-default-resources-file-role-", + { + "Ref": "AWS::Region" + } + ] + ] + } + } + }, + "CdkFileRoleDefaultPolicy621C7E5B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "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": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "BucketKey7092080A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CdkFileRoleDefaultPolicy621C7E5B", + "Roles": [ + { + "Ref": "CdkFileRoleE26CEABA" + } + ] + } + }, + "BucketKey7092080A": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:CancelKeyDeletion", + "kms:Create*", + "kms:Delete*", + "kms:Describe*", + "kms:Disable*", + "kms:Enable*", + "kms:Get*", + "kms:List*", + "kms:Put*", + "kms:Revoke*", + "kms:ScheduleKeyDeletion", + "kms:TagResource", + "kms:UntagResource", + "kms:Update*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "BucketKeyAlias69A0886F": { + "Type": "AWS::KMS::Alias", + "Properties": { + "AliasName": "alias/cdk-default-resources-staging", + "TargetKeyId": { + "Fn::GetAtt": [ + "BucketKey7092080A", + "Arn" + ] + } + } + }, + "CdkStagingBucket1636058C": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "BucketKey7092080A", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + }, + "BucketName": { + "Fn::Join": [ + "", + [ + "cdk-default-resources-staging-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "LifecycleConfiguration": { + "Rules": [ + { + "NoncurrentVersionExpiration": { + "NoncurrentDays": 365 + }, + "Status": "Enabled" + }, + { + "ExpirationInDays": 30, + "Prefix": "deploy-time/", + "Status": "Enabled" + } + ] + }, + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "CdkStagingBucketPolicy42BD1F92": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "CdkStagingBucket1636058C" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "CdkImageRoleF1394AC3": { + "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" + }, + "RoleName": { + "Fn::Join": [ + "", + [ + "cdk-default-resources-image-role-", + { + "Ref": "AWS::Region" + } + ] + ] + } + } + }, + "CdkImageRoleDefaultPolicy4A1572DE": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:CompleteLayerUpload", + "ecr:DescribeImages", + "ecr:DescribeRepositories", + "ecr:GetDownloadUrlForLayer", + "ecr:InitiateLayerUpload", + "ecr:PutImage", + "ecr:UploadLayerPart" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "defaultresourcesecrasset2FBE6B8A9", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "defaultresourcesecrasset9191BD6E", + "Arn" + ] + } + ] + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CdkImageRoleDefaultPolicy4A1572DE", + "Roles": [ + { + "Ref": "CdkImageRoleF1394AC3" + } + ] + } + }, + "defaultresourcesecrasset9191BD6E": { + "Type": "AWS::ECR::Repository", + "Properties": { + "LifecyclePolicy": { + "LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"description\":\"Garbage collect old image versions and keep the specified number of latest versions\",\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":3},\"action\":{\"type\":\"expire\"}}]}" + }, + "RepositoryName": "default-resources/ecr-asset" + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "defaultresourcesecrasset2FBE6B8A9": { + "Type": "AWS::ECR::Repository", + "Properties": { + "LifecyclePolicy": { + "LifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"description\":\"Garbage collect old image versions and keep the specified number of latest versions\",\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":3},\"action\":{\"type\":\"expire\"}}]}" + }, + "RepositoryName": "default-resources/ecr-asset-2" + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622/Dockerfile b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622/Dockerfile new file mode 100644 index 0000000000000..4a015204a5983 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622/Dockerfile @@ -0,0 +1,2 @@ +FROM public.ecr.aws/lambda/python:3.10 +CMD echo hello world \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622/index.py b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622/index.py new file mode 100644 index 0000000000000..ed0f110e2e61e --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622/index.py @@ -0,0 +1 @@ +print('hello') \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650/Dockerfile b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650/Dockerfile new file mode 100644 index 0000000000000..4a015204a5983 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650/Dockerfile @@ -0,0 +1,2 @@ +FROM public.ecr.aws/lambda/python:3.10 +CMD echo hello world \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650/index.py b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650/index.py new file mode 100644 index 0000000000000..ed0f110e2e61e --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650/index.py @@ -0,0 +1 @@ +print('hello') \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/cdk.out b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/cdk.out new file mode 100644 index 0000000000000..7925065efbcc4 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"31.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integ.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integ.json new file mode 100644 index 0000000000000..9eeaef0dfe700 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "31.0.0", + "testCases": { + "integ-tests/DefaultTest": { + "stacks": [ + "synthesize-default-resources" + ], + "assertionStack": "integ-tests/DefaultTest/DeployAssert", + "assertionStackName": "integtestsDefaultTestDeployAssert44C8D370" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.assets.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.assets.json new file mode 100644 index 0000000000000..7526fee9ff76c --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.assets.json @@ -0,0 +1,19 @@ +{ + "version": "31.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "integtestsDefaultTestDeployAssert44C8D370.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/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.template.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.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/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/manifest.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/manifest.json new file mode 100644 index 0000000000000..e9ac382233e75 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/manifest.json @@ -0,0 +1,213 @@ +{ + "version": "31.0.0", + "artifacts": { + "synthesize-default-resources.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "synthesize-default-resources.assets.json" + }, + "dependencies": [ + "StagingStack-default-resources-ACCOUNT-REGION" + ] + }, + "synthesize-default-resources": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "synthesize-default-resources.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "additionalDependencies": [ + "synthesize-default-resources.assets" + ], + "stackTemplateAssetObjectUrl": "s3://cdk-default-resources-staging-${AWS::AccountId}-${AWS::Region}/deploy-time/e21d11bec65be920861a56a86066cc88a0241d5cbe8324d0692ca982420e4cb0.json", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}" + } + }, + "dependencies": [ + "StagingStack-default-resources-ACCOUNT-REGION", + "synthesize-default-resources.assets" + ], + "metadata": { + "/synthesize-default-resources/lambda-s3/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "lambdas3ServiceRoleC9EDE33A" + } + ], + "/synthesize-default-resources/lambda-s3/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "lambdas342CE2BBD" + } + ], + "/synthesize-default-resources/lambda-ecr-1/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "lambdaecr1ServiceRoleA6BBC49F" + } + ], + "/synthesize-default-resources/lambda-ecr-1/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "lambdaecr1B33A3D15" + } + ], + "/synthesize-default-resources/lambda-ecr-1-copy/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "lambdaecr1copyServiceRole2A9FAF5F" + } + ], + "/synthesize-default-resources/lambda-ecr-1-copy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "lambdaecr1copyD39CDE9B" + } + ], + "/synthesize-default-resources/lambda-ecr-2/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "lambdaecr2ServiceRole2EA363D2" + } + ], + "/synthesize-default-resources/lambda-ecr-2/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "lambdaecr2615DAF68" + } + ] + }, + "displayName": "synthesize-default-resources" + }, + "StagingStack-default-resources-ACCOUNT-REGION": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "StagingStack-default-resources-ACCOUNT-REGION.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}", + "stackName": "StagingStack-default-resources" + }, + "metadata": { + "/StagingStack-default-resources-ACCOUNT-REGION/CdkFileRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CdkFileRoleE26CEABA" + } + ], + "/StagingStack-default-resources-ACCOUNT-REGION/CdkFileRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CdkFileRoleDefaultPolicy621C7E5B" + } + ], + "/StagingStack-default-resources-ACCOUNT-REGION/BucketKey/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "BucketKey7092080A" + } + ], + "/StagingStack-default-resources-ACCOUNT-REGION/BucketKey/Alias/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "BucketKeyAlias69A0886F" + } + ], + "/StagingStack-default-resources-ACCOUNT-REGION/CdkStagingBucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CdkStagingBucket1636058C" + } + ], + "/StagingStack-default-resources-ACCOUNT-REGION/CdkStagingBucket/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CdkStagingBucketPolicy42BD1F92" + } + ], + "/StagingStack-default-resources-ACCOUNT-REGION/CdkImageRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CdkImageRoleF1394AC3" + } + ], + "/StagingStack-default-resources-ACCOUNT-REGION/CdkImageRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CdkImageRoleDefaultPolicy4A1572DE" + } + ], + "/StagingStack-default-resources-ACCOUNT-REGION/default-resources--ecr-asset/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "defaultresourcesecrasset9191BD6E" + } + ], + "/StagingStack-default-resources-ACCOUNT-REGION/default-resources--ecr-asset-2/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "defaultresourcesecrasset2FBE6B8A9" + } + ] + }, + "displayName": "StagingStack-default-resources-ACCOUNT-REGION" + }, + "integtestsDefaultTestDeployAssert44C8D370.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "integtestsDefaultTestDeployAssert44C8D370.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "integtestsDefaultTestDeployAssert44C8D370": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "integtestsDefaultTestDeployAssert44C8D370.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": [ + "integtestsDefaultTestDeployAssert44C8D370.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": [ + "integtestsDefaultTestDeployAssert44C8D370.assets" + ], + "metadata": { + "/integ-tests/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/integ-tests/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "integ-tests/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.assets.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.assets.json new file mode 100644 index 0000000000000..c17a6ccdaa514 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.assets.json @@ -0,0 +1,57 @@ +{ + "version": "31.0.0", + "files": { + "68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650": { + "source": { + "path": "asset.68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-default-resources-staging-${AWS::AccountId}-${AWS::Region}", + "objectKey": "68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-default-resources-file-role-${AWS::Region}" + } + } + }, + "e21d11bec65be920861a56a86066cc88a0241d5cbe8324d0692ca982420e4cb0": { + "source": { + "path": "synthesize-default-resources.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-default-resources-staging-${AWS::AccountId}-${AWS::Region}", + "objectKey": "deploy-time/e21d11bec65be920861a56a86066cc88a0241d5cbe8324d0692ca982420e4cb0.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-default-resources-file-role-${AWS::Region}" + } + } + } + }, + "dockerImages": { + "ecr-asset-16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622": { + "source": { + "directory": "asset.16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622" + }, + "destinations": { + "current_account-current_region": { + "repositoryName": "default-resources/ecr-asset", + "imageTag": "16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-default-resources-image-role-${AWS::Region}" + } + } + }, + "ecr-asset-2-16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622": { + "source": { + "directory": "asset.16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622" + }, + "destinations": { + "current_account-current_region": { + "repositoryName": "default-resources/ecr-asset-2", + "imageTag": "16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-default-resources-image-role-${AWS::Region}" + } + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.template.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.template.json new file mode 100644 index 0000000000000..05ac9636afd0b --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.template.json @@ -0,0 +1,210 @@ +{ + "Resources": { + "lambdas3ServiceRoleC9EDE33A": { + "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" + ] + ] + } + ] + } + }, + "lambdas342CE2BBD": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-default-resources-staging-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650.zip" + }, + "Role": { + "Fn::GetAtt": [ + "lambdas3ServiceRoleC9EDE33A", + "Arn" + ] + }, + "Handler": "index.handler", + "Runtime": "python3.10" + }, + "DependsOn": [ + "lambdas3ServiceRoleC9EDE33A" + ] + }, + "lambdaecr1ServiceRoleA6BBC49F": { + "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" + ] + ] + } + ] + } + }, + "lambdaecr1B33A3D15": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ImageUri": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/default-resources/ecr-asset:16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622" + } + }, + "Role": { + "Fn::GetAtt": [ + "lambdaecr1ServiceRoleA6BBC49F", + "Arn" + ] + }, + "PackageType": "Image" + }, + "DependsOn": [ + "lambdaecr1ServiceRoleA6BBC49F" + ] + }, + "lambdaecr1copyServiceRole2A9FAF5F": { + "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" + ] + ] + } + ] + } + }, + "lambdaecr1copyD39CDE9B": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ImageUri": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/default-resources/ecr-asset:16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622" + } + }, + "Role": { + "Fn::GetAtt": [ + "lambdaecr1copyServiceRole2A9FAF5F", + "Arn" + ] + }, + "PackageType": "Image" + }, + "DependsOn": [ + "lambdaecr1copyServiceRole2A9FAF5F" + ] + }, + "lambdaecr2ServiceRole2EA363D2": { + "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" + ] + ] + } + ] + } + }, + "lambdaecr2615DAF68": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ImageUri": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/default-resources/ecr-asset-2:16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622" + } + }, + "Role": { + "Fn::GetAtt": [ + "lambdaecr2ServiceRole2EA363D2", + "Arn" + ] + }, + "PackageType": "Image" + }, + "DependsOn": [ + "lambdaecr2ServiceRole2EA363D2" + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/tree.json b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/tree.json new file mode 100644 index 0000000000000..4a76ae37e2e0d --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/tree.json @@ -0,0 +1,1225 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "synthesize-default-resources": { + "id": "synthesize-default-resources", + "path": "synthesize-default-resources", + "children": { + "lambda-s3": { + "id": "lambda-s3", + "path": "synthesize-default-resources/lambda-s3", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "synthesize-default-resources/lambda-s3/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "synthesize-default-resources/lambda-s3/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "synthesize-default-resources/lambda-s3/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-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "Code": { + "id": "Code", + "path": "synthesize-default-resources/lambda-s3/Code", + "children": { + "Stage": { + "id": "Stage", + "path": "synthesize-default-resources/lambda-s3/Code/Stage", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "synthesize-default-resources/lambda-s3/Code/AssetBucket", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3_assets.Asset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "synthesize-default-resources/lambda-s3/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "s3Bucket": { + "Fn::Sub": "cdk-default-resources-staging-${AWS::AccountId}-${AWS::Region}" + }, + "s3Key": "68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650.zip" + }, + "role": { + "Fn::GetAtt": [ + "lambdas3ServiceRoleC9EDE33A", + "Arn" + ] + }, + "handler": "index.handler", + "runtime": "python3.10" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.CfnFunction", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.Function", + "version": "0.0.0" + } + }, + "lambda-ecr-1": { + "id": "lambda-ecr-1", + "path": "synthesize-default-resources/lambda-ecr-1", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "synthesize-default-resources/lambda-ecr-1/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "synthesize-default-resources/lambda-ecr-1/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "synthesize-default-resources/lambda-ecr-1/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-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "AssetImage": { + "id": "AssetImage", + "path": "synthesize-default-resources/lambda-ecr-1/AssetImage", + "children": { + "Staging": { + "id": "Staging", + "path": "synthesize-default-resources/lambda-ecr-1/AssetImage/Staging", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "Repository": { + "id": "Repository", + "path": "synthesize-default-resources/lambda-ecr-1/AssetImage/Repository", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr.RepositoryBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr_assets.DockerImageAsset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "synthesize-default-resources/lambda-ecr-1/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "imageUri": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/default-resources/ecr-asset:16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622" + } + }, + "role": { + "Fn::GetAtt": [ + "lambdaecr1ServiceRoleA6BBC49F", + "Arn" + ] + }, + "packageType": "Image" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.CfnFunction", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.Function", + "version": "0.0.0" + } + }, + "lambda-ecr-1-copy": { + "id": "lambda-ecr-1-copy", + "path": "synthesize-default-resources/lambda-ecr-1-copy", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "synthesize-default-resources/lambda-ecr-1-copy/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "synthesize-default-resources/lambda-ecr-1-copy/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "synthesize-default-resources/lambda-ecr-1-copy/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-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "AssetImage": { + "id": "AssetImage", + "path": "synthesize-default-resources/lambda-ecr-1-copy/AssetImage", + "children": { + "Staging": { + "id": "Staging", + "path": "synthesize-default-resources/lambda-ecr-1-copy/AssetImage/Staging", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "Repository": { + "id": "Repository", + "path": "synthesize-default-resources/lambda-ecr-1-copy/AssetImage/Repository", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr.RepositoryBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr_assets.DockerImageAsset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "synthesize-default-resources/lambda-ecr-1-copy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "imageUri": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/default-resources/ecr-asset:16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622" + } + }, + "role": { + "Fn::GetAtt": [ + "lambdaecr1copyServiceRole2A9FAF5F", + "Arn" + ] + }, + "packageType": "Image" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.CfnFunction", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.Function", + "version": "0.0.0" + } + }, + "lambda-ecr-2": { + "id": "lambda-ecr-2", + "path": "synthesize-default-resources/lambda-ecr-2", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "synthesize-default-resources/lambda-ecr-2/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "synthesize-default-resources/lambda-ecr-2/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "synthesize-default-resources/lambda-ecr-2/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-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "AssetImage": { + "id": "AssetImage", + "path": "synthesize-default-resources/lambda-ecr-2/AssetImage", + "children": { + "Staging": { + "id": "Staging", + "path": "synthesize-default-resources/lambda-ecr-2/AssetImage/Staging", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "Repository": { + "id": "Repository", + "path": "synthesize-default-resources/lambda-ecr-2/AssetImage/Repository", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr.RepositoryBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr_assets.DockerImageAsset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "synthesize-default-resources/lambda-ecr-2/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "imageUri": { + "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/default-resources/ecr-asset-2:16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622" + } + }, + "role": { + "Fn::GetAtt": [ + "lambdaecr2ServiceRole2EA363D2", + "Arn" + ] + }, + "packageType": "Image" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.CfnFunction", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.Function", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "StagingStack-default-resources-ACCOUNT-REGION": { + "id": "StagingStack-default-resources-ACCOUNT-REGION", + "path": "StagingStack-default-resources-ACCOUNT-REGION", + "children": { + "CdkFileRole": { + "id": "CdkFileRole", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkFileRole", + "children": { + "ImportCdkFileRole": { + "id": "ImportCdkFileRole", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkFileRole/ImportCdkFileRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkFileRole/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" + }, + "roleName": { + "Fn::Join": [ + "", + [ + "cdk-default-resources-file-role-", + { + "Ref": "AWS::Region" + } + ] + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkFileRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkFileRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "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": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "BucketKey7092080A", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "CdkFileRoleDefaultPolicy621C7E5B", + "roles": [ + { + "Ref": "CdkFileRoleE26CEABA" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "BucketKey": { + "id": "BucketKey", + "path": "StagingStack-default-resources-ACCOUNT-REGION/BucketKey", + "children": { + "Resource": { + "id": "Resource", + "path": "StagingStack-default-resources-ACCOUNT-REGION/BucketKey/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::KMS::Key", + "aws:cdk:cloudformation:props": { + "keyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + }, + { + "Action": [ + "kms:CancelKeyDeletion", + "kms:Create*", + "kms:Delete*", + "kms:Describe*", + "kms:Disable*", + "kms:Enable*", + "kms:Get*", + "kms:List*", + "kms:Put*", + "kms:Revoke*", + "kms:ScheduleKeyDeletion", + "kms:TagResource", + "kms:UntagResource", + "kms:Update*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.CfnKey", + "version": "0.0.0" + } + }, + "Alias": { + "id": "Alias", + "path": "StagingStack-default-resources-ACCOUNT-REGION/BucketKey/Alias", + "children": { + "Resource": { + "id": "Resource", + "path": "StagingStack-default-resources-ACCOUNT-REGION/BucketKey/Alias/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::KMS::Alias", + "aws:cdk:cloudformation:props": { + "aliasName": "alias/cdk-default-resources-staging", + "targetKeyId": { + "Fn::GetAtt": [ + "BucketKey7092080A", + "Arn" + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.CfnAlias", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.Alias", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.Key", + "version": "0.0.0" + } + }, + "CdkStagingBucket": { + "id": "CdkStagingBucket", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkStagingBucket", + "children": { + "Resource": { + "id": "Resource", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkStagingBucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": { + "bucketEncryption": { + "serverSideEncryptionConfiguration": [ + { + "serverSideEncryptionByDefault": { + "sseAlgorithm": "aws:kms", + "kmsMasterKeyId": { + "Fn::GetAtt": [ + "BucketKey7092080A", + "Arn" + ] + } + } + } + ] + }, + "bucketName": { + "Fn::Join": [ + "", + [ + "cdk-default-resources-staging-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + }, + "lifecycleConfiguration": { + "rules": [ + { + "noncurrentVersionExpiration": { + "noncurrentDays": 365 + }, + "status": "Enabled" + }, + { + "expirationInDays": 30, + "prefix": "deploy-time/", + "status": "Enabled" + } + ] + }, + "versioningConfiguration": { + "status": "Enabled" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkStagingBucket/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkStagingBucket/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::BucketPolicy", + "aws:cdk:cloudformation:props": { + "bucket": { + "Ref": "CdkStagingBucket1636058C" + }, + "policyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Condition": { + "Bool": { + "aws:SecureTransport": "false" + } + }, + "Effect": "Deny", + "Principal": { + "AWS": "*" + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/cdk-hnb659fds-deploy-role-", + { + "Ref": "AWS::AccountId" + }, + "-", + { + "Ref": "AWS::Region" + } + ] + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "CdkStagingBucket1636058C", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.BucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0" + } + }, + "CdkImageRole": { + "id": "CdkImageRole", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkImageRole", + "children": { + "ImportCdkImageRole": { + "id": "ImportCdkImageRole", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkImageRole/ImportCdkImageRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkImageRole/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" + }, + "roleName": { + "Fn::Join": [ + "", + [ + "cdk-default-resources-image-role-", + { + "Ref": "AWS::Region" + } + ] + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkImageRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "StagingStack-default-resources-ACCOUNT-REGION/CdkImageRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:CompleteLayerUpload", + "ecr:DescribeImages", + "ecr:DescribeRepositories", + "ecr:GetDownloadUrlForLayer", + "ecr:InitiateLayerUpload", + "ecr:PutImage", + "ecr:UploadLayerPart" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "defaultresourcesecrasset2FBE6B8A9", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "defaultresourcesecrasset9191BD6E", + "Arn" + ] + } + ] + }, + { + "Action": "ecr:GetAuthorizationToken", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "policyName": "CdkImageRoleDefaultPolicy4A1572DE", + "roles": [ + { + "Ref": "CdkImageRoleF1394AC3" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "default-resources--ecr-asset": { + "id": "default-resources--ecr-asset", + "path": "StagingStack-default-resources-ACCOUNT-REGION/default-resources--ecr-asset", + "children": { + "Resource": { + "id": "Resource", + "path": "StagingStack-default-resources-ACCOUNT-REGION/default-resources--ecr-asset/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ECR::Repository", + "aws:cdk:cloudformation:props": { + "lifecyclePolicy": { + "lifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"description\":\"Garbage collect old image versions and keep the specified number of latest versions\",\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":3},\"action\":{\"type\":\"expire\"}}]}" + }, + "repositoryName": "default-resources/ecr-asset" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr.CfnRepository", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr.Repository", + "version": "0.0.0" + } + }, + "default-resources--ecr-asset-2": { + "id": "default-resources--ecr-asset-2", + "path": "StagingStack-default-resources-ACCOUNT-REGION/default-resources--ecr-asset-2", + "children": { + "Resource": { + "id": "Resource", + "path": "StagingStack-default-resources-ACCOUNT-REGION/default-resources--ecr-asset-2/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ECR::Repository", + "aws:cdk:cloudformation:props": { + "lifecyclePolicy": { + "lifecyclePolicyText": "{\"rules\":[{\"rulePriority\":1,\"description\":\"Garbage collect old image versions and keep the specified number of latest versions\",\"selection\":{\"tagStatus\":\"any\",\"countType\":\"imageCountMoreThan\",\"countNumber\":3},\"action\":{\"type\":\"expire\"}}]}" + }, + "repositoryName": "default-resources/ecr-asset-2" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr.CfnRepository", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ecr.Repository", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/app-staging-synthesizer-alpha.DefaultStagingStack", + "version": "0.0.0" + } + }, + "integ-tests": { + "id": "integ-tests", + "path": "integ-tests", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "integ-tests/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "integ-tests/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.26" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "integ-tests/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "integ-tests/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "integ-tests/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.2.26" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts new file mode 100644 index 0000000000000..e8f9aa1e27682 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts @@ -0,0 +1,51 @@ +import * as path from 'path'; +import * as integ from '@aws-cdk/integ-tests-alpha'; +import { App, Stack } from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import { AppStagingSynthesizer } from '../lib'; + +const app = new App(); + +const stack = new Stack(app, 'synthesize-default-resources', { + synthesizer: AppStagingSynthesizer.defaultResources({ + appId: 'default-resources', + }), +}); + +new lambda.Function(stack, 'lambda-s3', { + code: lambda.AssetCode.fromAsset(path.join(__dirname, 'assets')), + handler: 'index.handler', + runtime: lambda.Runtime.PYTHON_3_10, +}); + +new lambda.Function(stack, 'lambda-ecr-1', { + code: lambda.EcrImageCode.fromAssetImage(path.join(__dirname, 'assets'), { + assetName: 'ecr-asset', + }), + handler: lambda.Handler.FROM_IMAGE, + runtime: lambda.Runtime.FROM_IMAGE, +}); + +// This lambda will share the same published asset as lambda-ecr-1 +new lambda.Function(stack, 'lambda-ecr-1-copy', { + code: lambda.EcrImageCode.fromAssetImage(path.join(__dirname, 'assets'), { + assetName: 'ecr-asset', + }), + handler: lambda.Handler.FROM_IMAGE, + runtime: lambda.Runtime.FROM_IMAGE, +}); + +// This lambda will use a different published asset as lambda-ecr-1 +new lambda.Function(stack, 'lambda-ecr-2', { + code: lambda.EcrImageCode.fromAssetImage(path.join(__dirname, 'assets'), { + assetName: 'ecr-asset-2', + }), + handler: lambda.Handler.FROM_IMAGE, + runtime: lambda.Runtime.FROM_IMAGE, +}); + +new integ.IntegTest(app, 'integ-tests', { + testCases: [stack], +}); + +app.synth(); diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/per-env-staging-factory.test.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/per-env-staging-factory.test.ts new file mode 100644 index 0000000000000..a5001aaa9f2f4 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/per-env-staging-factory.test.ts @@ -0,0 +1,79 @@ +import { App, Stack } from 'aws-cdk-lib'; +import { APP_ID } from './util'; +import { AppStagingSynthesizer } from '../lib'; + +describe('per environment cache', () => { + test('same app, same env', () => { + // GIVEN + const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: APP_ID, + }), + }); + new Stack(app, 'Stack1', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + new Stack(app, 'Stack2', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + + // THEN + // stacks share the same staging resources + const asm = app.synth(); + expect(asm.stacks.length).toEqual(3); + const stagingResources = asm.stacks.filter((s) => s.displayName.startsWith('StagingStack')); + expect(stagingResources.length).toEqual(1); + }); + + test('same app, different envs', () => { + // GIVEN + const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: APP_ID, + }), + }); + new Stack(app, 'Stack1', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + new Stack(app, 'Stack2', { + env: { + account: '000000000000', + region: 'us-west-2', + }, + }); + + // THEN + // separate stacks for staging resources + const asm = app.synth(); + expect(asm.stacks.length).toEqual(4); + const stagingResources = asm.stacks.filter((s) => s.displayName.startsWith('StagingStack')); + expect(stagingResources.length).toEqual(2); + }); + + test('apps must be gnostic', () => { + // GIVEN + const app = new App({ + defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ + appId: APP_ID, + }), + }); + new Stack(app, 'Stack1', { + env: { + account: '000000000000', + region: 'us-east-1', + }, + }); + + // THEN + expect(() => new Stack(app, 'Stack2')).toThrowError(/It is not safe to use AppStagingSynthesizer for both environment-agnostic and environment-aware stacks at the same time./); + }); +}); diff --git a/packages/@aws-cdk/app-staging-synthesizer-alpha/test/util.ts b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/util.ts new file mode 100644 index 0000000000000..b90fbbe98b7e7 --- /dev/null +++ b/packages/@aws-cdk/app-staging-synthesizer-alpha/test/util.ts @@ -0,0 +1,16 @@ +import * as cxapi from 'aws-cdk-lib/cx-api'; + +export const CFN_CONTEXT = { + 'AWS::Region': 'the_region', + 'AWS::AccountId': 'the_account', + 'AWS::URLSuffix': 'domain.aws', +}; +export const APP_ID = 'appid'; + +export function isAssetManifest(x: cxapi.CloudArtifact): x is cxapi.AssetManifestArtifact { + return x instanceof cxapi.AssetManifestArtifact; +} + +export function last(xs?: A[]): A | undefined { + return xs ? xs[xs.length - 1] : undefined; +} diff --git a/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts b/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts index 3ca82c7194cb8..a626c596c814a 100644 --- a/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts +++ b/packages/aws-cdk-lib/aws-ecr-assets/lib/image-asset.ts @@ -266,6 +266,14 @@ export interface DockerImageAssetOptions extends FingerprintOptions, FileFingerp */ readonly outputs?: string[]; + /** + * Unique identifier of the docker image asset and its potential revisions. + * Required if using AppScopedStagingSynthesizer. + * + * @default - no asset name + */ + readonly assetName?: string; + /** * Cache from options to pass to the `docker build` command. * @@ -361,6 +369,14 @@ export class DockerImageAsset extends Construct implements IAsset { */ private readonly dockerOutputs?: string[]; + /** + * Unique identifier of the docker image asset and its potential revisions. + * Required if using AppScopedStagingSynthesizer. + * + * @default - no asset name + */ + private readonly assetName?: string; + /** * Cache from options to pass to the `docker build` command. */ @@ -453,11 +469,12 @@ export class DockerImageAsset extends Construct implements IAsset { : JSON.stringify(extraHash), }); - this.sourceHash = staging.assetHash; this.assetHash = staging.assetHash; + this.sourceHash = this.assetHash; const stack = Stack.of(this); this.assetPath = staging.relativeStagedPath(stack); + this.assetName = props.assetName; this.dockerBuildArgs = props.buildArgs; this.dockerBuildSecrets = props.buildSecrets; this.dockerBuildTarget = props.target; @@ -467,6 +484,7 @@ export class DockerImageAsset extends Construct implements IAsset { const location = stack.synthesizer.addDockerImageAsset({ directoryName: this.assetPath, + assetName: this.assetName, dockerBuildArgs: this.dockerBuildArgs, dockerBuildSecrets: this.dockerBuildSecrets, dockerBuildTarget: this.dockerBuildTarget, diff --git a/packages/aws-cdk-lib/aws-s3-assets/lib/asset.ts b/packages/aws-cdk-lib/aws-s3-assets/lib/asset.ts index a5b5f58e38d12..d20059505d674 100644 --- a/packages/aws-cdk-lib/aws-s3-assets/lib/asset.ts +++ b/packages/aws-cdk-lib/aws-s3-assets/lib/asset.ts @@ -34,6 +34,21 @@ export interface AssetOptions extends CopyOptions, cdk.FileCopyOptions, cdk.Asse * @deprecated see `assetHash` and `assetHashType` */ readonly sourceHash?: string; + + /** + * Whether or not the asset needs to exist beyond deployment time; i.e. + * are copied over to a different location and not needed afterwards. + * Setting this property to true has an impact on the lifecycle of the asset, + * because we will assume that it is safe to delete after the CloudFormation + * deployment succeeds. + * + * For example, Lambda Function assets are copied over to Lambda during + * deployment. Therefore, it is not necessary to store the asset in S3, so + * we consider those deployTime assets. + * + * @default false + */ + readonly deployTime?: boolean; } export interface AssetProps extends AssetOptions { @@ -147,6 +162,7 @@ export class Asset extends Construct implements cdk.IAsset { packaging: staging.packaging, sourceHash: this.sourceHash, fileName: this.assetPath, + deployTime: props.deployTime, }); this.s3BucketName = location.bucketName; diff --git a/packages/aws-cdk-lib/core/lib/assets.ts b/packages/aws-cdk-lib/core/lib/assets.ts index 99c9202b968ca..d4243b6f7a7b7 100644 --- a/packages/aws-cdk-lib/core/lib/assets.ts +++ b/packages/aws-cdk-lib/core/lib/assets.ts @@ -131,6 +131,21 @@ export interface FileAssetSource { * @default - Required if `fileName` is specified. */ readonly packaging?: FileAssetPackaging; + + /** + * Whether or not the asset needs to exist beyond deployment time; i.e. + * are copied over to a different location and not needed afterwards. + * Setting this property to true has an impact on the lifecycle of the asset, + * because we will assume that it is safe to delete after the CloudFormation + * deployment succeeds. + * + * For example, Lambda Function assets are copied over to Lambda during + * deployment. Therefore, it is not necessary to store the asset in S3, so + * we consider those deployTime assets. + * + * @default false + */ + readonly deployTime?: boolean; } export interface DockerImageAssetSource { @@ -242,18 +257,27 @@ export interface DockerImageAssetSource { */ readonly dockerOutputs?: string[]; + /** + * Unique identifier of the docker image asset and its potential revisions. + * Required if using AppScopedStagingSynthesizer. + * + * @default - no asset name + */ + readonly assetName?: string; + /** * Cache from options to pass to the `docker build` command. + * * @default - no cache from args are passed */ readonly dockerCacheFrom?: DockerCacheOption[]; /** * Cache to options to pass to the `docker build` command. + * * @default - no cache to args are passed */ readonly dockerCacheTo?: DockerCacheOption; - } /** diff --git a/packages/aws-cdk-lib/core/lib/helpers-internal/index.ts b/packages/aws-cdk-lib/core/lib/helpers-internal/index.ts index 9a36222b224cf..bc3f28e0107bb 100644 --- a/packages/aws-cdk-lib/core/lib/helpers-internal/index.ts +++ b/packages/aws-cdk-lib/core/lib/helpers-internal/index.ts @@ -2,3 +2,4 @@ export * from './cfn-parse'; // Other libraries are going to need this as well export { md5hash } from '../private/md5'; export * from './customize-roles'; +export * from './string-specializer'; \ No newline at end of file diff --git a/packages/aws-cdk-lib/core/lib/helpers-internal/string-specializer.ts b/packages/aws-cdk-lib/core/lib/helpers-internal/string-specializer.ts new file mode 100644 index 0000000000000..052cd2dafcd8a --- /dev/null +++ b/packages/aws-cdk-lib/core/lib/helpers-internal/string-specializer.ts @@ -0,0 +1,93 @@ +import * as cxapi from '../../../cx-api'; +import { Aws } from '../cfn-pseudo'; +import { Stack } from '../stack'; +import { Token } from '../token'; + +/** + * A "replace-all" function that doesn't require us escaping a literal string to a regex + */ +function replaceAll(s: string, search: string, replace: string) { + return s.split(search).join(replace); +} + +export class StringSpecializer { + /** + * Validate that the given string does not contain tokens + */ + public static validateNoTokens(s: string, what: string) { + if (Token.isUnresolved(s)) { + throw new Error(`${what} may not contain tokens; only the following literal placeholder strings are allowed: ` + [ + '${Qualifier}', + cxapi.EnvironmentPlaceholders.CURRENT_REGION, + cxapi.EnvironmentPlaceholders.CURRENT_ACCOUNT, + cxapi.EnvironmentPlaceholders.CURRENT_PARTITION, + ].join(', ') + `. Got: ${s}`); + } + } + + constructor(private readonly stack: Stack, private readonly qualifier: string) { } + + /** + * Function to replace placeholders in the input string as much as possible + * + * We replace: + * - ${Qualifier}: always + * - ${AWS::AccountId}, ${AWS::Region}: only if we have the actual values available + * - ${AWS::Partition}: never, since we never have the actual partition value. + */ + public specialize(s: string): string { + s = replaceAll(s, '${Qualifier}', this.qualifier); + return cxapi.EnvironmentPlaceholders.replace(s, { + region: resolvedOr(this.stack.region, cxapi.EnvironmentPlaceholders.CURRENT_REGION), + accountId: resolvedOr(this.stack.account, cxapi.EnvironmentPlaceholders.CURRENT_ACCOUNT), + partition: cxapi.EnvironmentPlaceholders.CURRENT_PARTITION, + }); + } + + /** + * Specialize the given string, make sure it doesn't contain tokens + */ + public specializeNoTokens(s: string, what: string): string { + StringSpecializer.validateNoTokens(s, what); + return this.specialize(s); + } + + /** + * Specialize only the qualifier + */ + public qualifierOnly(s: string): string { + return replaceAll(s, '${Qualifier}', this.qualifier); + } +} + +/** + * Return the given value if resolved or fall back to a default + */ +export function resolvedOr(x: string, def: A): string | A { + return Token.isUnresolved(x) ? def : x; +} + +const ASSET_TOKENS = ['${AWS::Partition}', '${AWS::Region}', '${AWS::AccountId}']; +const CFN_TOKENS = [Aws.PARTITION, Aws.REGION, Aws.ACCOUNT_ID]; + +/** + * Replaces CloudFormation Tokens (i.e. 'Aws.PARTITION') with corresponding + * Asset Tokens (i.e. '${AWS::Partition}'). + */ +export function translateCfnTokenToAssetToken(arn: string) { + for (let i = 0; i < CFN_TOKENS.length; i++) { + arn = replaceAll(arn, CFN_TOKENS[i], ASSET_TOKENS[i]); + } + return arn; +} + +/** + * Replaces Asset Tokens (i.e. '${AWS::Partition}') with corresponding + * CloudFormation Tokens (i.e. 'Aws.PARTITION'). + */ +export function translateAssetTokenToCfnToken(arn: string) { + for (let i = 0; i < ASSET_TOKENS.length; i++) { + arn = replaceAll(arn, ASSET_TOKENS[i], CFN_TOKENS[i]); + } + return arn; +} diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts index a9c882dcb1fa1..1017f172a850e 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts @@ -2,9 +2,7 @@ import * as crypto from 'crypto'; import { Node, IConstruct } from 'constructs'; import { ISynthesisSession } from './types'; import * as cxschema from '../../../cloud-assembly-schema'; -import * as cxapi from '../../../cx-api'; import { Stack } from '../stack'; -import { Token } from '../token'; /** * Shared logic of writing stack artifact to the Cloud Assembly @@ -126,46 +124,3 @@ export function assertBound(x: A | undefined): asserts x is NonNullable { function nonEmptyDict(xs: Record) { return Object.keys(xs).length > 0 ? xs : undefined; } - -/** - * A "replace-all" function that doesn't require us escaping a literal string to a regex - */ -function replaceAll(s: string, search: string, replace: string) { - return s.split(search).join(replace); -} - -export class StringSpecializer { - constructor(private readonly stack: Stack, private readonly qualifier: string) { - } - - /** - * Function to replace placeholders in the input string as much as possible - * - * We replace: - * - ${Qualifier}: always - * - ${AWS::AccountId}, ${AWS::Region}: only if we have the actual values available - * - ${AWS::Partition}: never, since we never have the actual partition value. - */ - public specialize(s: string): string { - s = replaceAll(s, '${Qualifier}', this.qualifier); - return cxapi.EnvironmentPlaceholders.replace(s, { - region: resolvedOr(this.stack.region, cxapi.EnvironmentPlaceholders.CURRENT_REGION), - accountId: resolvedOr(this.stack.account, cxapi.EnvironmentPlaceholders.CURRENT_ACCOUNT), - partition: cxapi.EnvironmentPlaceholders.CURRENT_PARTITION, - }); - } - - /** - * Specialize only the qualifier - */ - public qualifierOnly(s: string): string { - return replaceAll(s, '${Qualifier}', this.qualifier); - } -} - -/** - * Return the given value if resolved or fall back to a default - */ -export function resolvedOr(x: string, def: A): string | A { - return Token.isUnresolved(x) ? def : x; -} diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/asset-manifest-builder.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/asset-manifest-builder.ts index da10ef7e247d0..4ad800e23ba88 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/asset-manifest-builder.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/asset-manifest-builder.ts @@ -1,9 +1,9 @@ import * as fs from 'fs'; import * as path from 'path'; -import { resolvedOr } from './_shared'; import { ISynthesisSession } from './types'; import * as cxschema from '../../../cloud-assembly-schema'; import { FileAssetSource, FileAssetPackaging, DockerImageAssetSource } from '../assets'; +import { resolvedOr } from '../helpers-internal/string-specializer'; import { Stack } from '../stack'; /** @@ -61,7 +61,8 @@ export class AssetManifestBuilder { const imageTag = `${target.dockerTagPrefix ?? ''}${asset.sourceHash}`; // Add to manifest - return this.addDockerImageAsset(stack, asset.sourceHash, { + const sourceHash = asset.assetName ? `${asset.assetName}-${asset.sourceHash}` : asset.sourceHash; + return this.addDockerImageAsset(stack, sourceHash, { executable: asset.executable, directory: asset.directoryName, dockerBuildArgs: asset.dockerBuildArgs, @@ -131,6 +132,7 @@ export class AssetManifestBuilder { stack: Stack, session: ISynthesisSession, options: cxschema.AssetManifestOptions = {}, + dependencies: string[] = [], ): string { const artifactId = `${stack.artifactId}.assets`; const manifestFile = `${artifactId}.json`; @@ -150,6 +152,7 @@ export class AssetManifestBuilder { file: manifestFile, ...options, }, + dependencies: dependencies.length > 0 ? dependencies : undefined, }); return artifactId; diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts index ac1095c81f4ee..f9d949ff712b6 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts @@ -41,7 +41,7 @@ export interface BootstraplessSynthesizerProps { * synthesizer directly. */ export class BootstraplessSynthesizer extends DefaultStackSynthesizer { - constructor(props: BootstraplessSynthesizerProps) { + constructor(props: BootstraplessSynthesizerProps = {}) { super({ deployRoleArn: props.deployRoleArn, cloudFormationExecutionRole: props.cloudFormationExecutionRoleArn, diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/cli-credentials-synthesizer.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/cli-credentials-synthesizer.ts index d56604a35f21c..982530c851296 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/cli-credentials-synthesizer.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/cli-credentials-synthesizer.ts @@ -1,10 +1,11 @@ -import { assertBound, StringSpecializer } from './_shared'; +import { assertBound } from './_shared'; import { AssetManifestBuilder } from './asset-manifest-builder'; import { BOOTSTRAP_QUALIFIER_CONTEXT, DefaultStackSynthesizer } from './default-synthesizer'; import { StackSynthesizer } from './stack-synthesizer'; import { ISynthesisSession, IReusableStackSynthesizer, IBoundStackSynthesizer } from './types'; import * as cxapi from '../../../cx-api'; import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; +import { StringSpecializer } from '../helpers-internal/string-specializer'; import { Stack } from '../stack'; import { Token } from '../token'; diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/default-synthesizer.ts index 7140b7bffbfa5..2bfc7f6989a21 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/default-synthesizer.ts @@ -1,9 +1,10 @@ -import { assertBound, StringSpecializer } from './_shared'; +import { assertBound } from './_shared'; import { AssetManifestBuilder } from './asset-manifest-builder'; import { StackSynthesizer } from './stack-synthesizer'; import { ISynthesisSession, IReusableStackSynthesizer, IBoundStackSynthesizer } from './types'; import * as cxapi from '../../../cx-api'; import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; +import { StringSpecializer } from '../helpers-internal/string-specializer'; import { Stack } from '../stack'; import { Token } from '../token'; diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/stack-synthesizer.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/stack-synthesizer.ts index 444643eb04ff3..f8d5bb30ef344 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/stack-synthesizer.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/stack-synthesizer.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { addStackArtifactToAssembly, contentHash, resolvedOr } from './_shared'; +import { addStackArtifactToAssembly, contentHash } from './_shared'; import { IStackSynthesizer, ISynthesisSession } from './types'; import * as cxschema from '../../../cloud-assembly-schema'; import * as cxapi from '../../../cx-api'; @@ -8,6 +8,7 @@ import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, Fi import { Fn } from '../cfn-fn'; import { CfnParameter } from '../cfn-parameter'; import { CfnRule } from '../cfn-rule'; +import { resolvedOr } from '../helpers-internal/string-specializer'; import { Stack } from '../stack'; /** @@ -291,6 +292,7 @@ function stackTemplateFileAsset(stack: Stack, session: ISynthesisSession): FileA fileName: stack.templateFile, packaging: FileAssetPackaging.FILE, sourceHash, + deployTime: true, }; } diff --git a/packages/aws-cdk-lib/core/lib/stack.ts b/packages/aws-cdk-lib/core/lib/stack.ts index cb25807207a07..113c4ccd2bc9e 100644 --- a/packages/aws-cdk-lib/core/lib/stack.ts +++ b/packages/aws-cdk-lib/core/lib/stack.ts @@ -1726,7 +1726,7 @@ import { Names } from './names'; import { Reference } from './reference'; import { IResolvable } from './resolvable'; import { DefaultStackSynthesizer, IStackSynthesizer, ISynthesisSession, LegacyStackSynthesizer, BOOTSTRAP_QUALIFIER_CONTEXT, isReusableStackSynthesizer } from './stack-synthesizers'; -import { StringSpecializer } from './stack-synthesizers/_shared'; +import { StringSpecializer } from './helpers-internal/string-specializer'; import { Stage } from './stage'; import { ITaggable, TagManager } from './tag-manager'; import { Token, Tokenization } from './token'; diff --git a/packages/aws-cdk-lib/core/test/helpers-internal/string-specializer.test.ts b/packages/aws-cdk-lib/core/test/helpers-internal/string-specializer.test.ts new file mode 100644 index 0000000000000..382b3d268b148 --- /dev/null +++ b/packages/aws-cdk-lib/core/test/helpers-internal/string-specializer.test.ts @@ -0,0 +1,15 @@ +import { Aws } from '../../lib'; +import { translateAssetTokenToCfnToken, translateCfnTokenToAssetToken } from '../../lib/helpers-internal'; + +describe('translations between token kinds', () => { + const CfnTokenArn = `arn:${Aws.PARTITION}:resource:${Aws.REGION}:${Aws.ACCOUNT_ID}:name`; + const AssetTokenArn = 'arn:${AWS::Partition}:resource:${AWS::Region}:${AWS::AccountId}:name'; + + test('translateAssetTokenToCfnToken', () => { + expect(translateAssetTokenToCfnToken(AssetTokenArn)).toEqual(CfnTokenArn); + }); + + test('translateCfnTokenToAssetToken', () => { + expect(translateCfnTokenToAssetToken(CfnTokenArn)).toEqual(AssetTokenArn); + }); +}); \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts index e77fefa61083b..9eeebb0347c8d 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts @@ -174,6 +174,7 @@ export class SdkProvider { environment: cxapi.Environment, mode: Mode, options?: CredentialsOptions, + quiet = false, ): Promise { const env = await this.resolveEnvironment(environment); @@ -213,7 +214,8 @@ export class SdkProvider { // but if we can't then let's just try with available credentials anyway. if (baseCreds.source === 'correctDefault' || baseCreds.source === 'plugin') { debug(e.message); - warning(`${fmtObtainedCredentials(baseCreds)} could not be used to assume '${options.assumeRoleArn}', but are for the right account. Proceeding anyway.`); + const logger = quiet ? debug : warning; + logger(`${fmtObtainedCredentials(baseCreds)} could not be used to assume '${options.assumeRoleArn}', but are for the right account. Proceeding anyway.`); return { sdk: new SDK(baseCreds.credentials, env.region, this.sdkOptions), didAssumeRole: false }; } diff --git a/packages/aws-cdk/lib/util/asset-publishing.ts b/packages/aws-cdk/lib/util/asset-publishing.ts index c94c9bab94a94..7b4c4f943d9ff 100644 --- a/packages/aws-cdk/lib/util/asset-publishing.ts +++ b/packages/aws-cdk/lib/util/asset-publishing.ts @@ -169,6 +169,7 @@ export class PublishingAws implements cdk_assets.IAws { env, // region, name, account assumeRuleArn: options.assumeRoleArn, assumeRoleExternalId: options.assumeRoleExternalId, + quiet: options.quiet, }); const maybeSdk = this.sdkCache.get(cacheKey); @@ -179,7 +180,7 @@ export class PublishingAws implements cdk_assets.IAws { const sdk = (await this.aws.forEnvironment(env, Mode.ForWriting, { assumeRoleArn: options.assumeRoleArn, assumeRoleExternalId: options.assumeRoleExternalId, - })).sdk; + }, options.quiet)).sdk; this.sdkCache.set(cacheKey, sdk); return sdk; diff --git a/packages/cdk-assets/lib/aws.ts b/packages/cdk-assets/lib/aws.ts index 02bb67d41916c..4d9e731692d4e 100644 --- a/packages/cdk-assets/lib/aws.ts +++ b/packages/cdk-assets/lib/aws.ts @@ -18,6 +18,7 @@ export interface ClientOptions { region?: string; assumeRoleArn?: string; assumeRoleExternalId?: string; + quiet?: boolean; } /** diff --git a/packages/cdk-assets/lib/private/handlers/container-images.ts b/packages/cdk-assets/lib/private/handlers/container-images.ts index 0537c788970c9..670c813dd8b20 100644 --- a/packages/cdk-assets/lib/private/handlers/container-images.ts +++ b/packages/cdk-assets/lib/private/handlers/container-images.ts @@ -46,8 +46,13 @@ export class ContainerImageAssetHandler implements IAssetHandler { } public async isPublished(): Promise { - const initOnce = await this.initOnce(); - return initOnce.destinationAlreadyExists; + try { + const initOnce = await this.initOnce({ quiet: true }); + return initOnce.destinationAlreadyExists; + } catch (e: any) { + this.host.emitMessage(EventType.DEBUG, `${e.message}`); + } + return false; } public async publish(): Promise { @@ -68,13 +73,16 @@ export class ContainerImageAssetHandler implements IAssetHandler { await dockerForPushing.push(initOnce.imageUri); } - private async initOnce(): Promise { + private async initOnce(options: { quiet?: boolean } = {}): Promise { if (this.init) { return this.init; } const destination = await replaceAwsPlaceholders(this.asset.destination, this.host.aws); - const ecr = await this.host.aws.ecrClient(destination); + const ecr = await this.host.aws.ecrClient({ + ...destination, + quiet: options.quiet, + }); const account = async () => (await this.host.aws.discoverCurrentAccount())?.accountId; const repoUri = await repositoryUri(ecr, destination.repositoryName); diff --git a/packages/cdk-assets/lib/private/handlers/files.ts b/packages/cdk-assets/lib/private/handlers/files.ts index edc2addd61ada..fc538a82c95d0 100644 --- a/packages/cdk-assets/lib/private/handlers/files.ts +++ b/packages/cdk-assets/lib/private/handlers/files.ts @@ -33,7 +33,10 @@ export class FileAssetHandler implements IAssetHandler { const destination = await replaceAwsPlaceholders(this.asset.destination, this.host.aws); const s3Url = `s3://${destination.bucketName}/${destination.objectKey}`; try { - const s3 = await this.host.aws.s3Client(destination); + const s3 = await this.host.aws.s3Client({ + ...destination, + quiet: true, + }); this.host.emitMessage(EventType.CHECK, `Check ${s3Url}`); if (await objectExists(s3, destination.bucketName, destination.objectKey)) {