From ae21ecc2a72be14ececdf0c5b8649e49dc456b0c Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Fri, 19 May 2023 17:20:48 -0400 Subject: [PATCH] feat: new synthesizer separates assets out per CDK application (#24430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR introduces a new synthesizer inside the module `app-staging-synthesizer-alpha`. This new synthesizer produces staging resources alongside the CDK application and assets will be stored there. It removes the need for running `cdk bootstrap` before deploying a CDK app in a new account/region. Under the new synthesizer, assets between different CDK applications will be separated which means they can be cleaned up and lifecycle controlled independently. To get started, add the following to your CDK application: ```ts const app = new App({ defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({ appId: 'my-app-id', // put a unique id here }), }); ``` The new format of staging resources will look something like this: ```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 feature is heavily experimental and the API may break in the future. It does not work with CDK Pipelines yet. Depended on #25536. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../.eslintrc.js | 4 + .../app-staging-synthesizer-alpha/.gitignore | 22 + .../app-staging-synthesizer-alpha/.npmignore | 14 + .../app-staging-synthesizer-alpha/LICENSE | 201 +++ .../app-staging-synthesizer-alpha/NOTICE | 2 + .../app-staging-synthesizer-alpha/README.md | 389 ++++++ .../adr/resource-names.md | 30 + .../jest.config.js | 4 + .../lib/app-staging-synthesizer.ts | 372 +++++ .../lib/bootstrap-roles.ts | 130 ++ .../lib/default-staging-stack.ts | 425 ++++++ .../lib/index.ts | 4 + .../lib/per-env-staging-factory.ts | 32 + .../lib/private/app-global.ts | 33 + .../lib/private/no-tokens.ts | 9 + .../lib/staging-stack.ts | 120 ++ .../package.json | 101 ++ .../rosetta/default.ts-fixture | 22 + .../rosetta/with-custom-staging.ts-fixture | 44 + .../test/app-staging-synthesizer.test.ts | 493 +++++++ .../test/assets/Dockerfile | 2 + .../test/assets/index.py | 1 + .../test/bootstrap-roles.test.ts | 189 +++ .../test/default-staging-stack.test.ts | 44 + .../test/evaluate-cfn.ts | 114 ++ ...ult-resources-ACCOUNT-REGION.template.json | 472 +++++++ .../Dockerfile | 2 + .../index.py | 1 + .../Dockerfile | 2 + .../index.py | 1 + .../cdk.out | 1 + .../integ.json | 12 + ...efaultTestDeployAssert44C8D370.assets.json | 19 + ...aultTestDeployAssert44C8D370.template.json | 36 + .../manifest.json | 213 +++ .../synthesize-default-resources.assets.json | 57 + ...synthesize-default-resources.template.json | 210 +++ .../tree.json | 1225 +++++++++++++++++ .../test/integ.synth-default-resources.ts | 51 + .../test/per-env-staging-factory.test.ts | 79 ++ .../test/util.ts | 16 + .../aws-ecr-assets/lib/image-asset.ts | 20 +- .../aws-cdk-lib/aws-s3-assets/lib/asset.ts | 16 + packages/aws-cdk-lib/core/lib/assets.ts | 26 +- .../core/lib/helpers-internal/index.ts | 1 + .../helpers-internal/string-specializer.ts | 93 ++ .../core/lib/stack-synthesizers/_shared.ts | 45 - .../asset-manifest-builder.ts | 7 +- .../bootstrapless-synthesizer.ts | 2 +- .../cli-credentials-synthesizer.ts | 3 +- .../stack-synthesizers/default-synthesizer.ts | 3 +- .../stack-synthesizers/stack-synthesizer.ts | 4 +- packages/aws-cdk-lib/core/lib/stack.ts | 2 +- .../string-specializer.test.ts | 15 + .../aws-cdk/lib/api/aws-auth/sdk-provider.ts | 4 +- packages/aws-cdk/lib/util/asset-publishing.ts | 3 +- packages/cdk-assets/lib/aws.ts | 1 + .../lib/private/handlers/container-images.ts | 16 +- .../cdk-assets/lib/private/handlers/files.ts | 5 +- 59 files changed, 5403 insertions(+), 61 deletions(-) create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/.eslintrc.js create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/.gitignore create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/.npmignore create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/LICENSE create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/NOTICE create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/README.md create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/adr/resource-names.md create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/jest.config.js create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/lib/app-staging-synthesizer.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/lib/bootstrap-roles.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/lib/default-staging-stack.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/lib/index.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/lib/per-env-staging-factory.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/lib/private/app-global.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/lib/private/no-tokens.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/lib/staging-stack.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/package.json create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/rosetta/default.ts-fixture create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/rosetta/with-custom-staging.ts-fixture create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/app-staging-synthesizer.test.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/assets/Dockerfile create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/assets/index.py create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/bootstrap-roles.test.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/default-staging-stack.test.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/evaluate-cfn.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/StagingStack-default-resources-ACCOUNT-REGION.template.json create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622/Dockerfile create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.16624c2a162b07c5cc0e2c59c484f638bac238ca558ccbdc2aa0e0535df3e622/index.py create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650/Dockerfile create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/asset.68539effc3f7ad46fff9765606c2a01b7f7965833643ab37e62799f19a37f650/index.py create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integ.json create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.assets.json create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/integtestsDefaultTestDeployAssert44C8D370.template.json create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.assets.json create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/synthesize-default-resources.template.json create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.js.snapshot/tree.json create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/integ.synth-default-resources.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/per-env-staging-factory.test.ts create mode 100644 packages/@aws-cdk/app-staging-synthesizer-alpha/test/util.ts create mode 100644 packages/aws-cdk-lib/core/lib/helpers-internal/string-specializer.ts create mode 100644 packages/aws-cdk-lib/core/test/helpers-internal/string-specializer.test.ts 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)) {