From 8b2a2b2b2248c1461c31164d8ae4b2d23f2f78cd Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Tue, 14 Jan 2025 12:31:07 +0000 Subject: [PATCH 1/2] refactor(cli): move library code into temporary private package --- .../@aws-cdk/tmp-toolkit-helpers/.eslintrc.js | 4 + .../@aws-cdk/tmp-toolkit-helpers/.gitignore | 33 + .../@aws-cdk/tmp-toolkit-helpers/.npmignore | 27 + packages/@aws-cdk/tmp-toolkit-helpers/LICENSE | 201 ++++ packages/@aws-cdk/tmp-toolkit-helpers/NOTICE | 16 + .../@aws-cdk/tmp-toolkit-helpers/README.md | 22 + .../@aws-cdk/tmp-toolkit-helpers/generate.sh | 29 + .../tmp-toolkit-helpers/jest.config.js | 9 + .../tmp-toolkit-helpers/lib/api/assets.ts | 138 +++ .../lib/api/aws-auth/account-cache.ts | 109 ++ .../lib/api/aws-auth/awscli-compatible.ts | 299 ++++++ .../lib/api/aws-auth/cached.ts | 22 + .../lib/api/aws-auth/credential-plugins.ts | 176 ++++ .../lib/api/aws-auth/index.ts | 2 + .../lib/api/aws-auth/provider-caching.ts | 26 + .../lib/api/aws-auth/sdk-logger.ts | 142 +++ .../lib/api/aws-auth/sdk-provider.ts | 531 ++++++++++ .../lib/api/aws-auth/sdk.ts | 987 ++++++++++++++++++ .../lib/api/aws-auth/user-agent.ts | 17 + .../lib/api/aws-auth/util.ts | 19 + .../api/bootstrap/bootstrap-environment.ts | 377 +++++++ .../lib/api/bootstrap/bootstrap-props.ts | 141 +++ .../lib/api/bootstrap/deploy-bootstrap.ts | 167 +++ .../lib/api/bootstrap/index.ts | 2 + .../lib/api/bootstrap/legacy-template.ts | 79 ++ .../lib/api/cxapp/cloud-assembly.ts | 387 +++++++ .../lib/api/cxapp/cloud-executable.ts | 126 +++ .../lib/api/cxapp/environments.ts | 69 ++ .../tmp-toolkit-helpers/lib/api/cxapp/exec.ts | 310 ++++++ .../lib/api/deploy-stack.ts | 876 ++++++++++++++++ .../lib/api/deployments.ts | 801 ++++++++++++++ .../tmp-toolkit-helpers/lib/api/diff.ts | 203 ++++ .../lib/api/environment-access.ts | 274 +++++ .../lib/api/environment-resources.ts | 240 +++++ .../api/evaluate-cloudformation-template.ts | 578 ++++++++++ .../garbage-collection/garbage-collector.ts | 772 ++++++++++++++ .../garbage-collection/progress-printer.ts | 76 ++ .../api/garbage-collection/stack-refresh.ts | 191 ++++ .../lib/api/hotswap-deployments.ts | 493 +++++++++ .../api/hotswap/appsync-mapping-templates.ts | 189 ++++ .../lib/api/hotswap/code-build-projects.ts | 79 ++ .../lib/api/hotswap/common.ts | 263 +++++ .../lib/api/hotswap/ecs-services.ts | 179 ++++ .../lib/api/hotswap/lambda-functions.ts | 403 +++++++ .../lib/api/hotswap/s3-bucket-deployments.ts | 135 +++ .../hotswap/stepfunctions-state-machines.ts | 48 + .../tmp-toolkit-helpers/lib/api/import.ts | 464 ++++++++ .../tmp-toolkit-helpers/lib/api/index.ts | 5 + .../lib/api/list-stacks.ts | 94 ++ .../tmp-toolkit-helpers/lib/api/logging.ts | 280 +++++ .../lib/api/logs/find-cloudwatch-logs.ts | 145 +++ .../lib/api/logs/logs-monitor.ts | 235 +++++ .../lib/api/nested-stack-helpers.ts | 157 +++ .../tmp-toolkit-helpers/lib/api/notices.ts | 518 +++++++++ .../lib/api/plugin/context-provider-plugin.ts | 7 + .../lib/api/plugin/index.ts | 2 + .../lib/api/plugin/mode.ts | 6 + .../lib/api/plugin/plugin.ts | 111 ++ .../tmp-toolkit-helpers/lib/api/settings.ts | 542 ++++++++++ .../lib/api/toolkit-info.ts | 223 ++++ .../lib/api/util/checks.ts | 81 ++ .../lib/api/util/cloudformation.ts | 742 +++++++++++++ .../cloudformation/stack-activity-monitor.ts | 799 ++++++++++++++ .../util/cloudformation/stack-event-poller.ts | 190 ++++ .../api/util/cloudformation/stack-status.ts | 98 ++ .../lib/api/util/display.ts | 87 ++ .../lib/api/util/placeholders.ts | 28 + .../lib/api/util/rwlock.ts | 201 ++++ .../lib/api/util/string-manipulation.ts | 7 + .../lib/api/util/template-body-parameter.ts | 135 +++ .../lib/context-providers/ami.ts | 53 + .../context-providers/availability-zones.ts | 27 + .../endpoint-service-availability-zones.ts | 31 + .../lib/context-providers/hosted-zones.ts | 71 ++ .../lib/context-providers/index.ts | 121 +++ .../lib/context-providers/keys.ts | 61 ++ .../lib/context-providers/load-balancers.ts | 200 ++++ .../lib/context-providers/security-groups.ts | 80 ++ .../lib/context-providers/ssm-parameters.ts | 58 + .../lib/context-providers/vpcs.ts | 388 +++++++ .../lib/toolkit/cli-io-host.ts | 200 ++++ .../tmp-toolkit-helpers/lib/toolkit/error.ts | 48 + .../tmp-toolkit-helpers/lib/util/archive.ts | 93 ++ .../tmp-toolkit-helpers/lib/util/arrays.ts | 31 + .../lib/util/asset-manifest-builder.ts | 32 + .../lib/util/asset-publishing.ts | 242 +++++ .../tmp-toolkit-helpers/lib/util/bool.ts | 9 + .../lib/util/console-formatters.ts | 44 + .../lib/util/content-hash.ts | 44 + .../lib/util/directories.ts | 77 ++ .../tmp-toolkit-helpers/lib/util/error.ts | 19 + .../tmp-toolkit-helpers/lib/util/index.ts | 5 + .../tmp-toolkit-helpers/lib/util/npm.ts | 20 + .../tmp-toolkit-helpers/lib/util/objects.ts | 176 ++++ .../tmp-toolkit-helpers/lib/util/os.ts | 97 ++ .../tmp-toolkit-helpers/lib/util/parallel.ts | 44 + .../tmp-toolkit-helpers/lib/util/serialize.ts | 35 + .../tmp-toolkit-helpers/lib/util/tables.ts | 7 + .../tmp-toolkit-helpers/lib/util/tracing.ts | 53 + .../tmp-toolkit-helpers/lib/util/tree.ts | 58 + .../lib/util/type-brands.ts | 44 + .../tmp-toolkit-helpers/lib/util/types.ts | 32 + .../lib/util/validate-notification-arn.ts | 6 + .../lib/util/version-range.ts | 38 + .../lib/util/work-graph-builder.ts | 176 ++++ .../lib/util/work-graph-types.ts | 56 + .../lib/util/work-graph.ts | 401 +++++++ .../tmp-toolkit-helpers/lib/util/yaml-cfn.ts | 59 ++ .../@aws-cdk/tmp-toolkit-helpers/package.json | 114 ++ .../bootstrap/bootstrap-template.yaml | 692 ++++++++++++ .../init-templates/.no-packagejson-validator | 0 .../resources/init-templates/LICENSE | 16 + .../app/csharp/.template.gitignore | 342 ++++++ .../init-templates/app/csharp/README.md | 14 + .../app/csharp/cdk.template.json | 15 + .../src/%name.PascalCased%.template.sln | 18 + .../%name.PascalCased%.template.csproj | 20 + .../%name.PascalCased%Stack.template.cs | 13 + .../%name.PascalCased%/GlobalSuppressions.cs | 1 + .../%name.PascalCased%/Program.template.cs | 44 + .../app/fsharp/.template.gitignore | 342 ++++++ .../init-templates/app/fsharp/README.md | 18 + .../app/fsharp/cdk.template.json | 14 + .../src/%name.PascalCased%.template.sln | 18 + .../%name.PascalCased%.template.fsproj | 25 + .../%name.PascalCased%Stack.template.fs | 8 + .../%name.PascalCased%/Program.template.fs | 11 + .../init-templates/app/go/%name%.template.go | 70 ++ .../app/go/%name%_test.template.go | 26 + .../init-templates/app/go/.template.gitignore | 19 + .../resources/init-templates/app/go/README.md | 12 + .../init-templates/app/go/cdk.template.json | 13 + .../init-templates/app/go/go.template.mod | 9 + .../resources/init-templates/app/info.json | 4 + .../app/java/.template.gitignore | 13 + .../init-templates/app/java/README.md | 18 + .../init-templates/app/java/cdk.json | 13 + .../resources/init-templates/app/java/pom.xml | 60 ++ .../myorg/%name.PascalCased%App.template.java | 42 + .../%name.PascalCased%Stack.template.java | 24 + .../%name.PascalCased%Test.template.java | 26 + .../app/javascript/.template.gitignore | 5 + .../app/javascript/.template.npmignore | 3 + .../init-templates/app/javascript/README.md | 12 + .../app/javascript/bin/%name%.template.js | 21 + .../app/javascript/cdk.template.json | 15 + .../app/javascript/jest.config.js | 3 + .../javascript/lib/%name%-stack.template.js | 23 + .../app/javascript/package.json | 20 + .../javascript/test/%name%.test.template.js | 17 + .../%name.PythonModule%_stack.template.py | 19 + .../python/%name.PythonModule%/__init__.py | 0 .../app/python/.template.gitignore | 10 + .../app/python/README.template.md | 58 + .../init-templates/app/python/app.template.py | 28 + .../app/python/cdk.template.json | 15 + .../app/python/requirements-dev.txt | 1 + .../app/python/requirements.txt | 2 + .../init-templates/app/python/source.bat | 13 + .../app/python/tests/__init__.py | 0 .../app/python/tests/unit/__init__.py | 0 ...test_%name.PythonModule%_stack.template.py | 15 + .../app/typescript/.template.gitignore | 8 + .../app/typescript/.template.npmignore | 6 + .../init-templates/app/typescript/README.md | 14 + .../app/typescript/bin/%name%.template.ts | 20 + .../app/typescript/cdk.template.json | 17 + .../app/typescript/jest.config.js | 8 + .../typescript/lib/%name%-stack.template.ts | 16 + .../app/typescript/package.json | 26 + .../typescript/test/%name%.test.template.ts | 17 + .../app/typescript/tsconfig.json | 31 + .../resources/init-templates/lib/info.json | 4 + .../lib/typescript/.template.gitignore | 8 + .../lib/typescript/.template.npmignore | 6 + .../lib/typescript/README.template.md | 12 + .../lib/typescript/jest.config.js | 8 + .../lib/typescript/lib/index.template.ts | 21 + .../lib/typescript/package.json | 24 + .../typescript/test/%name%.test.template.ts | 18 + .../lib/typescript/tsconfig.json | 31 + .../sample-app/csharp/.template.gitignore | 342 ++++++ .../sample-app/csharp/README.template.md | 19 + .../sample-app/csharp/cdk.template.json | 15 + .../src/%name.PascalCased%.template.sln | 18 + .../%name.PascalCased%.template.csproj | 20 + .../%name.PascalCased%Stack.template.cs | 24 + .../%name.PascalCased%/GlobalSuppressions.cs | 1 + .../%name.PascalCased%/Program.template.cs | 15 + .../sample-app/fsharp/.template.gitignore | 342 ++++++ .../sample-app/fsharp/README.template.md | 20 + .../sample-app/fsharp/cdk.template.json | 14 + .../src/%name.PascalCased%.template.sln | 18 + .../%name.PascalCased%.template.fsproj | 25 + .../%name.PascalCased%Stack.template.fs | 14 + .../%name.PascalCased%/Program.template.fs | 11 + .../sample-app/go/%name%.template.go | 73 ++ .../sample-app/go/%name%_test.template.go | 25 + .../sample-app/go/.template.gitignore | 19 + .../init-templates/sample-app/go/README.md | 12 + .../sample-app/go/cdk.template.json | 13 + .../sample-app/go/go.template.mod | 9 + .../init-templates/sample-app/info.json | 4 + .../sample-app/java/.template.gitignore | 13 + .../sample-app/java/README.template.md | 19 + .../init-templates/sample-app/java/cdk.json | 13 + .../init-templates/sample-app/java/pom.xml | 55 + .../myorg/%name.PascalCased%App.template.java | 13 + .../%name.PascalCased%Stack.template.java | 29 + .../%name.PascalCased%StackTest.template.java | 27 + .../sample-app/javascript/.template.gitignore | 5 + .../sample-app/javascript/.template.npmignore | 3 + .../sample-app/javascript/README.template.md | 13 + .../javascript/bin/%name%.template.js | 6 + .../sample-app/javascript/cdk.template.json | 15 + .../sample-app/javascript/jest.config.js | 3 + .../javascript/lib/%name%-stack.template.js | 25 + .../sample-app/javascript/package.json | 20 + .../javascript/test/%name%.test.template.js | 16 + .../sample-app/javascript/tsconfig.json | 34 + .../%name.PythonModule%_stack.template.py | 26 + .../python/%name.PythonModule%/__init__.py | 0 .../sample-app/python/.template.gitignore | 22 + .../sample-app/python/README.template.md | 65 ++ .../sample-app/python/app.template.py | 11 + .../sample-app/python/cdk.template.json | 15 + .../sample-app/python/requirements-dev.txt | 1 + .../sample-app/python/requirements.txt | 2 + .../sample-app/python/source.bat | 13 + .../sample-app/python/tests/__init__.py | 0 .../sample-app/python/tests/unit/__init__.py | 0 ...test_%name.PythonModule%_stack.template.py | 21 + .../sample-app/typescript/.template.gitignore | 8 + .../sample-app/typescript/.template.npmignore | 6 + .../sample-app/typescript/README.template.md | 15 + .../typescript/bin/%name%.template.ts | 6 + .../sample-app/typescript/cdk.template.json | 17 + .../sample-app/typescript/jest.config.js | 8 + .../typescript/lib/%name%-stack.template.ts | 19 + .../sample-app/typescript/package.json | 26 + .../typescript/test/%name%.test.template.ts | 17 + .../sample-app/typescript/tsconfig.json | 31 + .../tmp-toolkit-helpers/tsconfig.json | 31 + 243 files changed, 23031 insertions(+) create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/.eslintrc.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/.npmignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/LICENSE create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/NOTICE create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/README.md create mode 100755 packages/@aws-cdk/tmp-toolkit-helpers/generate.sh create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/jest.config.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/assets.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/account-cache.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/awscli-compatible.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/cached.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/credential-plugins.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/index.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/provider-caching.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk-logger.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk-provider.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/user-agent.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/util.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/bootstrap-environment.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/bootstrap-props.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/deploy-bootstrap.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/index.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/legacy-template.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/cloud-assembly.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/cloud-executable.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/environments.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/exec.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/deploy-stack.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/deployments.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/diff.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/environment-access.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/environment-resources.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/evaluate-cloudformation-template.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/garbage-collection/garbage-collector.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/garbage-collection/progress-printer.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/garbage-collection/stack-refresh.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/hotswap-deployments.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/hotswap/appsync-mapping-templates.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/hotswap/code-build-projects.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/hotswap/common.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/hotswap/ecs-services.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/hotswap/lambda-functions.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/hotswap/s3-bucket-deployments.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/hotswap/stepfunctions-state-machines.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/import.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/index.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/list-stacks.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/logging.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/logs/find-cloudwatch-logs.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/logs/logs-monitor.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/nested-stack-helpers.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/notices.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/plugin/context-provider-plugin.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/plugin/index.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/plugin/mode.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/plugin/plugin.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/settings.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/toolkit-info.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/util/checks.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/util/cloudformation.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/util/cloudformation/stack-activity-monitor.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/util/cloudformation/stack-event-poller.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/util/cloudformation/stack-status.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/util/display.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/util/placeholders.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/util/rwlock.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/util/string-manipulation.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/api/util/template-body-parameter.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/context-providers/ami.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/context-providers/availability-zones.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/context-providers/endpoint-service-availability-zones.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/context-providers/hosted-zones.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/context-providers/index.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/context-providers/keys.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/context-providers/load-balancers.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/context-providers/security-groups.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/context-providers/ssm-parameters.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/context-providers/vpcs.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/toolkit/cli-io-host.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/toolkit/error.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/archive.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/arrays.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/asset-manifest-builder.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/asset-publishing.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/bool.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/console-formatters.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/content-hash.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/directories.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/error.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/index.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/npm.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/objects.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/os.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/parallel.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/serialize.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/tables.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/tracing.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/tree.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/type-brands.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/types.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/validate-notification-arn.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/version-range.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/work-graph-builder.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/work-graph-types.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/work-graph.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/lib/util/yaml-cfn.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/package.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/bootstrap/bootstrap-template.yaml create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/.no-packagejson-validator create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/LICENSE create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/csharp/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/csharp/README.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/csharp/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/csharp/src/%name.PascalCased%.template.sln create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/csharp/src/%name.PascalCased%/%name.PascalCased%.template.csproj create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/csharp/src/%name.PascalCased%/%name.PascalCased%Stack.template.cs create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/csharp/src/%name.PascalCased%/GlobalSuppressions.cs create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/csharp/src/%name.PascalCased%/Program.template.cs create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/fsharp/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/fsharp/README.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/fsharp/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/fsharp/src/%name.PascalCased%.template.sln create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/fsharp/src/%name.PascalCased%/%name.PascalCased%.template.fsproj create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/fsharp/src/%name.PascalCased%/%name.PascalCased%Stack.template.fs create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/fsharp/src/%name.PascalCased%/Program.template.fs create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/go/%name%.template.go create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/go/%name%_test.template.go create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/go/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/go/README.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/go/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/go/go.template.mod create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/info.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/java/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/java/README.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/java/cdk.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/java/pom.xml create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/java/src/main/java/com/myorg/%name.PascalCased%App.template.java create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/java/src/main/java/com/myorg/%name.PascalCased%Stack.template.java create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/java/src/test/java/com/myorg/%name.PascalCased%Test.template.java create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/javascript/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/javascript/.template.npmignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/javascript/README.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/javascript/bin/%name%.template.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/javascript/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/javascript/jest.config.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/javascript/lib/%name%-stack.template.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/javascript/package.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/javascript/test/%name%.test.template.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/%name.PythonModule%/__init__.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/README.template.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/app.template.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/requirements-dev.txt create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/requirements.txt create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/source.bat create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/tests/__init__.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/tests/unit/__init__.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/python/tests/unit/test_%name.PythonModule%_stack.template.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/typescript/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/typescript/.template.npmignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/typescript/README.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/typescript/bin/%name%.template.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/typescript/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/typescript/jest.config.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/typescript/lib/%name%-stack.template.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/typescript/package.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/typescript/test/%name%.test.template.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/app/typescript/tsconfig.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/lib/info.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/lib/typescript/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/lib/typescript/.template.npmignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/lib/typescript/README.template.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/lib/typescript/jest.config.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/lib/typescript/lib/index.template.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/lib/typescript/package.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/lib/typescript/test/%name%.test.template.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/lib/typescript/tsconfig.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/csharp/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/csharp/README.template.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/csharp/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/csharp/src/%name.PascalCased%.template.sln create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/csharp/src/%name.PascalCased%/%name.PascalCased%.template.csproj create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/csharp/src/%name.PascalCased%/%name.PascalCased%Stack.template.cs create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/csharp/src/%name.PascalCased%/GlobalSuppressions.cs create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/csharp/src/%name.PascalCased%/Program.template.cs create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/fsharp/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/fsharp/README.template.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/fsharp/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/fsharp/src/%name.PascalCased%.template.sln create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/fsharp/src/%name.PascalCased%/%name.PascalCased%.template.fsproj create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/fsharp/src/%name.PascalCased%/%name.PascalCased%Stack.template.fs create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/fsharp/src/%name.PascalCased%/Program.template.fs create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/go/%name%.template.go create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/go/%name%_test.template.go create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/go/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/go/README.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/go/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/go/go.template.mod create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/info.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/java/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/java/README.template.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/java/cdk.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/java/pom.xml create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/java/src/main/java/com/myorg/%name.PascalCased%App.template.java create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/java/src/main/java/com/myorg/%name.PascalCased%Stack.template.java create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/java/src/test/java/com/myorg/%name.PascalCased%StackTest.template.java create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/javascript/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/javascript/.template.npmignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/javascript/README.template.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/javascript/bin/%name%.template.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/javascript/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/javascript/jest.config.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/javascript/lib/%name%-stack.template.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/javascript/package.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/javascript/test/%name%.test.template.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/javascript/tsconfig.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/%name.PythonModule%/__init__.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/README.template.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/app.template.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/requirements-dev.txt create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/requirements.txt create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/source.bat create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/tests/__init__.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/tests/unit/__init__.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/python/tests/unit/test_%name.PythonModule%_stack.template.py create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/typescript/.template.gitignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/typescript/.template.npmignore create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/typescript/README.template.md create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/typescript/bin/%name%.template.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/typescript/cdk.template.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/typescript/jest.config.js create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/typescript/lib/%name%-stack.template.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/typescript/package.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/typescript/test/%name%.test.template.ts create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/resources/init-templates/sample-app/typescript/tsconfig.json create mode 100644 packages/@aws-cdk/tmp-toolkit-helpers/tsconfig.json diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/.eslintrc.js b/packages/@aws-cdk/tmp-toolkit-helpers/.eslintrc.js new file mode 100644 index 0000000000000..edea2a8c01665 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/.eslintrc.js @@ -0,0 +1,4 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +baseConfig.ignorePatterns.push('resources/init-templates/**/typescript/**/*.ts'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/.gitignore b/packages/@aws-cdk/tmp-toolkit-helpers/.gitignore new file mode 100644 index 0000000000000..ea294d133e5c1 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/.gitignore @@ -0,0 +1,33 @@ +*.js +*.js.map +*.d.ts +*.d.ts.map +*.gz +node_modules +dist +.jsii + +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +.LAST_PACKAGE +*.snk +junit.xml + +assets.json +npm-shrinkwrap.json +!.eslintrc.js +!jest.config.js + +# Wasm +lib/**/*.wasm + +# Resources +resources/build-info.json +!resources/init-templates/**/javascript/**/* +resources/init-templates/.init-version.json +resources/init-templates/.recommended-feature-flags.json +resources/init-templates/**/*.hook.js +resources/init-templates/**/*.hook.d.ts +resources/init-templates/.*.json diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/.npmignore b/packages/@aws-cdk/tmp-toolkit-helpers/.npmignore new file mode 100644 index 0000000000000..7a6f445dbbe78 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/.npmignore @@ -0,0 +1,27 @@ +# Ignore artifacts +dist +.LAST_PACKAGE +.LAST_BUILD +*.snk +*.ts +!*.d.ts +!*.js +coverage +.nyc_output +*.tgz + +# Ignore configs and test files +.eslintrc.js +tsconfig.json +*.tsbuildinfo +junit.xml + +# Include .jsii +!.jsii + +# exclude cdk artifacts +**/cdk.out +**/*.snapshot + +# include all resources +resource/** diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/LICENSE b/packages/@aws-cdk/tmp-toolkit-helpers/LICENSE new file mode 100644 index 0000000000000..5ccf0c6780bab --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/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-2025 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/tmp-toolkit-helpers/NOTICE b/packages/@aws-cdk/tmp-toolkit-helpers/NOTICE new file mode 100644 index 0000000000000..62c4308b020b7 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/NOTICE @@ -0,0 +1,16 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2025 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. + +Third party attributions of this package can be found in the THIRD_PARTY_LICENSES file diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/README.md b/packages/@aws-cdk/tmp-toolkit-helpers/README.md new file mode 100644 index 0000000000000..5f7d5ea01e20c --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/README.md @@ -0,0 +1,22 @@ +# AWS CDK Programmatic Toolkit Helpers + + +--- + +![@aws-cdk/tmp-toolkit-helpers: Experimental](https://img.shields.io/badge/@aws--cdk/toolkit-experimental-important.svg?style=for-the-badge) + +> The APIs 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. + +--- + + + +A temporary private package to hold CLI and Toolkit library files. +The package is used by the Programmatic Toolkit package `@aws-cdk/toolkit` and the AWS CDK CLI `aws-cdk`. + +**Do not rely on this package!** +It's contents will gradually be moved over to `@aws-cdk/toolkit` and eventually the package will seize to exist. diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/generate.sh b/packages/@aws-cdk/tmp-toolkit-helpers/generate.sh new file mode 100755 index 0000000000000..3473b975f5eaf --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/generate.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -euo pipefail + +commit=${CODEBUILD_RESOLVED_SOURCE_VERSION:-} +# CODEBUILD_RESOLVED_SOURCE_VERSION is not defined (i.e. local build or CodePipeline build), +# use the HEAD commit hash +if [ -z "${commit}" ]; then + commit="$(git rev-parse --verify HEAD)" +fi + +cat > resources/build-info.json < resources/init-templates/.init-version.json + +# Copy the recommended-feature-flags.json file out from aws-cdk-lib. +path=$(node -p 'require.resolve("aws-cdk-lib/recommended-feature-flags.json")') +cp $path resources/init-templates/.recommended-feature-flags.json + +# Copy the service-spec database out from @aws-cdk/aws-service-spec. +path=$(node -p 'require.resolve("@aws-cdk/aws-service-spec/db.json.gz")') +cp $path ./resources diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/jest.config.js b/packages/@aws-cdk/tmp-toolkit-helpers/jest.config.js new file mode 100644 index 0000000000000..69a39a35b373a --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/jest.config.js @@ -0,0 +1,9 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + ...baseConfig.coverageThreshold.global, + }, + }, +}; diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/assets.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/assets.ts new file mode 100644 index 0000000000000..9dc1af0510d5c --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/assets.ts @@ -0,0 +1,138 @@ +import * as path from 'path'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as chalk from 'chalk'; +import { EnvironmentResources } from './environment-resources'; +import { debug } from './logging'; +import { ToolkitInfo } from './toolkit-info'; +import { ToolkitError } from '../toolkit/error'; +import { AssetManifestBuilder } from '../util/asset-manifest-builder'; + +/** + * Take the metadata assets from the given stack and add them to the given asset manifest + * + * Returns the CloudFormation parameters that need to be sent to the template to + * pass Asset coordinates. + */ +// eslint-disable-next-line max-len +export async function addMetadataAssetsToManifest(stack: cxapi.CloudFormationStackArtifact, assetManifest: AssetManifestBuilder, envResources: EnvironmentResources, reuse?: string[]): Promise> { + reuse = reuse || []; + const assets = stack.assets; + + if (assets.length === 0) { + return {}; + } + + const toolkitInfo = await envResources.lookupToolkit(); + if (!toolkitInfo.found) { + // eslint-disable-next-line max-len + throw new ToolkitError(`This stack uses assets, so the toolkit stack must be deployed to the environment (Run "${chalk.blue('cdk bootstrap ' + stack.environment!.name)}")`); + } + + const params: Record = {}; + + for (const asset of assets) { + // FIXME: Should have excluded by construct path here instead of by unique ID, preferably using + // minimatch so we can support globs. Maybe take up during artifact refactoring. + const reuseAsset = reuse.indexOf(asset.id) > -1; + + if (reuseAsset) { + debug(`Reusing asset ${asset.id}: ${JSON.stringify(asset)}`); + continue; + } + + debug(`Preparing asset ${asset.id}: ${JSON.stringify(asset)}`); + if (!stack.assembly) { + throw new ToolkitError('Unexpected: stack assembly is required in order to find assets in assembly directory'); + } + + Object.assign(params, await prepareAsset(asset, assetManifest, envResources, toolkitInfo)); + } + + return params; +} + +// eslint-disable-next-line max-len +async function prepareAsset(asset: cxschema.AssetMetadataEntry, assetManifest: AssetManifestBuilder, envResources: EnvironmentResources, toolkitInfo: ToolkitInfo): Promise> { + switch (asset.packaging) { + case 'zip': + case 'file': + return prepareFileAsset( + asset, + assetManifest, + toolkitInfo, + asset.packaging === 'zip' ? cxschema.FileAssetPackaging.ZIP_DIRECTORY : cxschema.FileAssetPackaging.FILE); + case 'container-image': + return prepareDockerImageAsset(asset, assetManifest, envResources); + default: + // eslint-disable-next-line max-len + throw new ToolkitError(`Unsupported packaging type: ${(asset as any).packaging}. You might need to upgrade your aws-cdk toolkit to support this asset type.`); + } +} + +function prepareFileAsset( + asset: cxschema.FileAssetMetadataEntry, + assetManifest: AssetManifestBuilder, + toolkitInfo: ToolkitInfo, + packaging: cxschema.FileAssetPackaging): Record { + + const extension = packaging === cxschema.FileAssetPackaging.ZIP_DIRECTORY ? '.zip' : path.extname(asset.path); + const baseName = `${asset.sourceHash}${extension}`; + // Simplify key: assets/abcdef/abcdef.zip is kinda silly and unnecessary, so if they're the same just pick one component. + const s3Prefix = asset.id === asset.sourceHash ? 'assets/' : `assets/${asset.id}/`; + const key = `${s3Prefix}${baseName}`; + const s3url = `s3://${toolkitInfo.bucketName}/${key}`; + + debug(`Storing asset ${asset.path} at ${s3url}`); + + assetManifest.addFileAsset(asset.sourceHash, { + path: asset.path, + packaging, + }, { + bucketName: toolkitInfo.bucketName, + objectKey: key, + }); + + return { + [asset.s3BucketParameter]: toolkitInfo.bucketName, + [asset.s3KeyParameter]: `${s3Prefix}${cxapi.ASSET_PREFIX_SEPARATOR}${baseName}`, + [asset.artifactHashParameter]: asset.sourceHash, + }; +} + +async function prepareDockerImageAsset( + asset: cxschema.ContainerImageAssetMetadataEntry, + assetManifest: AssetManifestBuilder, + envResources: EnvironmentResources): Promise> { + + // Pre-1.21.0, repositoryName can be specified by the user or can be left out, in which case we make + // a per-asset repository which will get adopted and cleaned up along with the stack. + // Post-1.21.0, repositoryName will always be specified and it will be a shared repository between + // all assets, and asset will have imageTag specified as well. Validate the combination. + if (!asset.imageNameParameter && (!asset.repositoryName || !asset.imageTag)) { + throw new ToolkitError('Invalid Docker image asset configuration: "repositoryName" and "imageTag" are required when "imageNameParameter" is left out'); + } + + const repositoryName = asset.repositoryName ?? 'cdk/' + asset.id.replace(/[:/]/g, '-').toLowerCase(); + + // Make sure the repository exists, since the 'cdk-assets' tool will not create it for us. + const { repositoryUri } = await envResources.prepareEcrRepository(repositoryName); + const imageTag = asset.imageTag ?? asset.sourceHash; + + assetManifest.addDockerImageAsset(asset.sourceHash, { + directory: asset.path, + dockerBuildArgs: asset.buildArgs, + dockerBuildSsh: asset.buildSsh, + dockerBuildTarget: asset.target, + dockerFile: asset.file, + networkMode: asset.networkMode, + platform: asset.platform, + dockerOutputs: asset.outputs, + }, { + repositoryName, + imageTag, + }); + + if (!asset.imageNameParameter) { return {}; } + return { [asset.imageNameParameter]: `${repositoryUri}:${imageTag}` }; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/account-cache.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/account-cache.ts new file mode 100644 index 0000000000000..309b0733dd894 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/account-cache.ts @@ -0,0 +1,109 @@ +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { Account } from './sdk-provider'; +import { cdkCacheDir } from '../../util/directories'; +import { debug } from '../logging'; + +/** + * Disk cache which maps access key IDs to account IDs. + * Usage: + * cache.get(accessKey) => accountId | undefined + * cache.put(accessKey, accountId) + */ +export class AccountAccessKeyCache { + /** + * Max number of entries in the cache, after which the cache will be reset. + */ + public static readonly MAX_ENTRIES = 1000; + + private readonly cacheFile: string; + + /** + * @param filePath Path to the cache file + */ + constructor(filePath?: string) { + this.cacheFile = filePath || path.join(cdkCacheDir(), 'accounts_partitions.json'); + } + + /** + * Tries to fetch the account ID from cache. If it's not in the cache, invokes + * the resolver function which should retrieve the account ID and return it. + * Then, it will be stored into disk cache returned. + * + * Example: + * + * const accountId = cache.fetch(accessKey, async () => { + * return await fetchAccountIdFromSomewhere(accessKey); + * }); + * + * @param accessKeyId + * @param resolver + */ + public async fetch(accessKeyId: string, resolver: () => Promise) { + // try to get account ID based on this access key ID from disk. + const cached = await this.get(accessKeyId); + if (cached) { + debug(`Retrieved account ID ${cached.accountId} from disk cache`); + return cached; + } + + // if it's not in the cache, resolve and put in cache. + const account = await resolver(); + if (account) { + await this.put(accessKeyId, account); + } + + return account; + } + + /** Get the account ID from an access key or undefined if not in cache */ + public async get(accessKeyId: string): Promise { + const map = await this.loadMap(); + return map[accessKeyId]; + } + + /** Put a mapping between access key and account ID */ + public async put(accessKeyId: string, account: Account) { + let map = await this.loadMap(); + + // nuke cache if it's too big. + if (Object.keys(map).length >= AccountAccessKeyCache.MAX_ENTRIES) { + map = {}; + } + + map[accessKeyId] = account; + await this.saveMap(map); + } + + private async loadMap(): Promise<{ [accessKeyId: string]: Account }> { + try { + return await fs.readJson(this.cacheFile); + } catch (e: any) { + // File doesn't exist or is not readable. This is a cache, + // pretend we successfully loaded an empty map. + if (e.code === 'ENOENT' || e.code === 'EACCES') { + return {}; + } + // File is not JSON, could be corrupted because of concurrent writes. + // Again, an empty cache is fine. + if (e instanceof SyntaxError) { + return {}; + } + throw e; + } + } + + private async saveMap(map: { [accessKeyId: string]: Account }) { + try { + await fs.ensureFile(this.cacheFile); + await fs.writeJson(this.cacheFile, map, { spaces: 2 }); + } catch (e: any) { + // File doesn't exist or file/dir isn't writable. This is a cache, + // if we can't write it then too bad. + if (e.code === 'ENOENT' || e.code === 'EACCES' || e.code === 'EROFS') { + return; + } + throw e; + } + } +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/awscli-compatible.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/awscli-compatible.ts new file mode 100644 index 0000000000000..a6959020e7adf --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/awscli-compatible.ts @@ -0,0 +1,299 @@ +import { createCredentialChain, fromEnv, fromIni, fromNodeProviderChain } from '@aws-sdk/credential-providers'; +import { MetadataService } from '@aws-sdk/ec2-metadata-service'; +import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler'; +import { loadSharedConfigFiles } from '@smithy/shared-ini-file-loader'; +import { AwsCredentialIdentityProvider, Logger } from '@smithy/types'; +import * as promptly from 'promptly'; +import { ProxyAgent } from 'proxy-agent'; +import { makeCachingProvider } from './provider-caching'; +import type { SdkHttpOptions } from './sdk-provider'; +import { readIfPossible } from './util'; +import { AuthenticationError } from '../../toolkit/error'; +import { debug } from '../logging'; + +const DEFAULT_CONNECTION_TIMEOUT = 10000; +const DEFAULT_TIMEOUT = 300000; + +/** + * Behaviors to match AWS CLI + * + * See these links: + * + * https://docs.aws.amazon.com/cli/latest/topic/config-vars.html + * https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html + */ +export class AwsCliCompatible { + /** + * Build an AWS CLI-compatible credential chain provider + * + * The credential chain returned by this function is always caching. + */ + public static async credentialChainBuilder( + options: CredentialChainOptions = {}, + ): Promise { + const clientConfig = { + requestHandler: AwsCliCompatible.requestHandlerBuilder(options.httpOptions), + customUserAgent: 'aws-cdk', + logger: options.logger, + }; + + // Super hacky solution to https://github.com/aws/aws-cdk/issues/32510, proposed by the SDK team. + // + // Summary of the problem: we were reading the region from the config file and passing it to + // the credential providers. However, in the case of SSO, this makes the credential provider + // use that region to do the SSO flow, which is incorrect. The region that should be used for + // that is the one set in the sso_session section of the config file. + // + // The idea here: the "clientConfig" is for configuring the inner auth client directly, + // and has the highest priority, whereas "parentClientConfig" is the upper data client + // and has lower priority than the sso_region but still higher priority than STS global region. + const parentClientConfig = { + region: await this.region(options.profile), + }; + /** + * The previous implementation matched AWS CLI behavior: + * + * If a profile is explicitly set using `--profile`, + * we use that to the exclusion of everything else. + * + * Note: this does not apply to AWS_PROFILE, + * environment credentials still take precedence over AWS_PROFILE + */ + if (options.profile) { + return makeCachingProvider(fromIni({ + profile: options.profile, + ignoreCache: true, + mfaCodeProvider: tokenCodeFn, + clientConfig, + parentClientConfig, + logger: options.logger, + })); + } + + const envProfile = process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE; + + /** + * Env AWS - EnvironmentCredentials with string AWS + * Env Amazon - EnvironmentCredentials with string AMAZON + * Profile Credentials - PatchedSharedIniFileCredentials with implicit profile, credentials file, http options, and token fn + * SSO with implicit profile only + * SharedIniFileCredentials with implicit profile and preferStaticCredentials true (profile with source_profile) + * Shared Credential file that points to Environment Credentials with AWS prefix + * Shared Credential file that points to EC2 Metadata + * Shared Credential file that points to ECS Credentials + * SSO Credentials - SsoCredentials with implicit profile and http options + * ProcessCredentials with implicit profile + * ECS Credentials - ECSCredentials with no input OR Web Identity - TokenFileWebIdentityCredentials with no input OR EC2 Metadata - EC2MetadataCredentials with no input + * + * These translate to: + * fromEnv() + * fromSSO()/fromIni() + * fromProcess() + * fromContainerMetadata() + * fromTokenFile() + * fromInstanceMetadata() + * + * The NodeProviderChain is already cached. + */ + const nodeProviderChain = fromNodeProviderChain({ + profile: envProfile, + clientConfig, + parentClientConfig, + logger: options.logger, + mfaCodeProvider: tokenCodeFn, + ignoreCache: true, + }); + + return shouldPrioritizeEnv() + ? createCredentialChain(fromEnv(), nodeProviderChain).expireAfter(60 * 60_000) + : nodeProviderChain; + } + + public static requestHandlerBuilder(options: SdkHttpOptions = {}): NodeHttpHandlerOptions { + const agent = this.proxyAgent(options); + + return { + connectionTimeout: DEFAULT_CONNECTION_TIMEOUT, + requestTimeout: DEFAULT_TIMEOUT, + httpsAgent: agent, + httpAgent: agent, + }; + } + + public static proxyAgent(options: SdkHttpOptions) { + // Force it to use the proxy provided through the command line. + // Otherwise, let the ProxyAgent auto-detect the proxy using environment variables. + const getProxyForUrl = options.proxyAddress != null + ? () => Promise.resolve(options.proxyAddress!) + : undefined; + + return new ProxyAgent({ + ca: tryGetCACert(options.caBundlePath), + getProxyForUrl, + }); + } + + /** + * Attempts to get the region from a number of sources and falls back to us-east-1 if no region can be found, + * as is done in the AWS CLI. + * + * The order of priority is the following: + * + * 1. Environment variables specifying region, with both an AWS prefix and AMAZON prefix + * to maintain backwards compatibility, and without `DEFAULT` in the name because + * Lambda and CodeBuild set the $AWS_REGION variable. + * 2. Regions listed in the Shared Ini Files - First checking for the profile provided + * and then checking for the default profile. + * 3. IMDS instance identity region from the Metadata Service. + * 4. us-east-1 + */ + public static async region(maybeProfile?: string): Promise { + const defaultRegion = 'us-east-1'; + const profile = maybeProfile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || 'default'; + + const region = + process.env.AWS_REGION || + process.env.AMAZON_REGION || + process.env.AWS_DEFAULT_REGION || + process.env.AMAZON_DEFAULT_REGION || + (await getRegionFromIni(profile)) || + (await regionFromMetadataService()); + + if (!region) { + const usedProfile = !profile ? '' : ` (profile: "${profile}")`; + debug( + `Unable to determine AWS region from environment or AWS configuration${usedProfile}, defaulting to '${defaultRegion}'`, + ); + return defaultRegion; + } + + return region; + } +} + +/** + * Looks up the region of the provided profile. If no region is present, + * it will attempt to lookup the default region. + * @param profile The profile to use to lookup the region + * @returns The region for the profile or default profile, if present. Otherwise returns undefined. + */ +async function getRegionFromIni(profile: string): Promise { + const sharedFiles = await loadSharedConfigFiles({ ignoreCache: true }); + + // Priority: + // + // credentials come before config because aws-cli v1 behaves like that. + // + // 1. profile-region-in-credentials + // 2. profile-region-in-config + // 3. default-region-in-credentials + // 4. default-region-in-config + + return getRegionFromIniFile(profile, sharedFiles.credentialsFile) + ?? getRegionFromIniFile(profile, sharedFiles.configFile) + ?? getRegionFromIniFile('default', sharedFiles.credentialsFile) + ?? getRegionFromIniFile('default', sharedFiles.configFile); + +} + +function getRegionFromIniFile(profile: string, data?: any) { + return data?.[profile]?.region; +} + +function tryGetCACert(bundlePath?: string) { + const path = bundlePath || caBundlePathFromEnvironment(); + if (path) { + debug('Using CA bundle path: %s', path); + return readIfPossible(path); + } + return undefined; +} + +/** + * Find and return a CA certificate bundle path to be passed into the SDK. + */ +function caBundlePathFromEnvironment(): string | undefined { + if (process.env.aws_ca_bundle) { + return process.env.aws_ca_bundle; + } + if (process.env.AWS_CA_BUNDLE) { + return process.env.AWS_CA_BUNDLE; + } + return undefined; +} + +/** + * We used to support both AWS and AMAZON prefixes for these environment variables. + * + * Adding this for backward compatibility. + */ +function shouldPrioritizeEnv() { + const id = process.env.AWS_ACCESS_KEY_ID || process.env.AMAZON_ACCESS_KEY_ID; + const key = process.env.AWS_SECRET_ACCESS_KEY || process.env.AMAZON_SECRET_ACCESS_KEY; + + if (!!id && !!key) { + process.env.AWS_ACCESS_KEY_ID = id; + process.env.AWS_SECRET_ACCESS_KEY = key; + + const sessionToken = process.env.AWS_SESSION_TOKEN ?? process.env.AMAZON_SESSION_TOKEN; + if (sessionToken) { + process.env.AWS_SESSION_TOKEN = sessionToken; + } + + return true; + } + + return false; +} + +/** + * The MetadataService class will attempt to fetch the instance identity document from + * IMDSv2 first, and then will attempt v1 as a fallback. + * + * If this fails, we will use us-east-1 as the region so no error should be thrown. + * @returns The region for the instance identity + */ +async function regionFromMetadataService() { + debug('Looking up AWS region in the EC2 Instance Metadata Service (IMDS).'); + try { + const metadataService = new MetadataService({ + httpOptions: { + timeout: 1000, + }, + }); + + await metadataService.fetchMetadataToken(); + const document = await metadataService.request('/latest/dynamic/instance-identity/document', {}); + return JSON.parse(document).region; + } catch (e) { + debug(`Unable to retrieve AWS region from IMDS: ${e}`); + } +} + +export interface CredentialChainOptions { + readonly profile?: string; + readonly httpOptions?: SdkHttpOptions; + readonly logger?: Logger; +} + +/** + * Ask user for MFA token for given serial + * + * Result is send to callback function for SDK to authorize the request + */ +async function tokenCodeFn(serialArn: string): Promise { + debug('Require MFA token for serial ARN', serialArn); + try { + const token: string = await promptly.prompt(`MFA token for ${serialArn}: `, { + trim: true, + default: '', + }); + debug('Successfully got MFA token from user'); + return token; + } catch (err: any) { + debug('Failed to get MFA token', err); + const e = new AuthenticationError(`Error fetching MFA token: ${err.message ?? err}`); + e.name = 'SharedIniFileCredentialsProviderFailure'; + throw e; + } +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/cached.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/cached.ts new file mode 100644 index 0000000000000..d1a9982aa233e --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/cached.ts @@ -0,0 +1,22 @@ +/** + * Cache the result of a function on an object + * + * We could have used @decorators to make this nicer but we don't use them anywhere yet, + * so let's keep it simple and readable. + */ +export function cached(obj: A, sym: symbol, fn: () => B): B { + if (!(sym in obj)) { + (obj as any)[sym] = fn(); + } + return (obj as any)[sym]; +} + +/** + * Like 'cached', but async + */ +export async function cachedAsync(obj: A, sym: symbol, fn: () => Promise): Promise { + if (!(sym in obj)) { + (obj as any)[sym] = await fn(); + } + return (obj as any)[sym]; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/credential-plugins.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/credential-plugins.ts new file mode 100644 index 0000000000000..28aa40d60afb5 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/credential-plugins.ts @@ -0,0 +1,176 @@ +import { inspect, format } from 'util'; +import type { CredentialProviderSource, ForReading, ForWriting, PluginProviderResult, SDKv2CompatibleCredentials, SDKv3CompatibleCredentialProvider, SDKv3CompatibleCredentials } from '@aws-cdk/cli-plugin-contract'; +import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@smithy/types'; +import { credentialsAboutToExpire, makeCachingProvider } from './provider-caching'; +import { AuthenticationError } from '../../toolkit/error'; +import { formatErrorMessage } from '../../util/error'; +import { debug, warning, info } from '../logging'; +import { Mode } from '../plugin/mode'; +import { PluginHost } from '../plugin/plugin'; + +/** + * Cache for credential providers. + * + * Given an account and an operating mode (read or write) will return an + * appropriate credential provider for credentials for the given account. The + * credential provider will be cached so that multiple AWS clients for the same + * environment will not make multiple network calls to obtain credentials. + * + * Will use default credentials if they are for the right account; otherwise, + * all loaded credential provider plugins will be tried to obtain credentials + * for the given account. + */ +export class CredentialPlugins { + private readonly cache: { [key: string]: PluginCredentialsFetchResult | undefined } = {}; + private readonly host: PluginHost; + + constructor(host?: PluginHost) { + this.host = host ?? PluginHost.instance; + } + + public async fetchCredentialsFor(awsAccountId: string, mode: Mode): Promise { + const key = `${awsAccountId}-${mode}`; + if (!(key in this.cache)) { + this.cache[key] = await this.lookupCredentials(awsAccountId, mode); + } + return this.cache[key]; + } + + public get availablePluginNames(): string[] { + return this.host.credentialProviderSources.map((s) => s.name); + } + + private async lookupCredentials(awsAccountId: string, mode: Mode): Promise { + const triedSources: CredentialProviderSource[] = []; + // Otherwise, inspect the various credential sources we have + for (const source of this.host.credentialProviderSources) { + let available: boolean; + try { + available = await source.isAvailable(); + } catch (e: any) { + // This shouldn't happen, but let's guard against it anyway + warning(`Uncaught exception in ${source.name}: ${formatErrorMessage(e)}`); + available = false; + } + + if (!available) { + debug('Credentials source %s is not available, ignoring it.', source.name); + continue; + } + triedSources.push(source); + let canProvide: boolean; + try { + canProvide = await source.canProvideCredentials(awsAccountId); + } catch (e: any) { + // This shouldn't happen, but let's guard against it anyway + warning(`Uncaught exception in ${source.name}: ${formatErrorMessage(e)}`); + canProvide = false; + } + if (!canProvide) { + continue; + } + debug(`Using ${source.name} credentials for account ${awsAccountId}`); + + return { + credentials: await v3ProviderFromPlugin(() => source.getProvider(awsAccountId, mode as ForReading | ForWriting, { + supportsV3Providers: true, + })), + pluginName: source.name, + }; + } + return undefined; + } +} + +/** + * Result from trying to fetch credentials from the Plugin host + */ +export interface PluginCredentialsFetchResult { + /** + * SDK-v3 compatible credential provider + */ + readonly credentials: AwsCredentialIdentityProvider; + + /** + * Name of plugin that successfully provided credentials + */ + readonly pluginName: string; +} + +/** + * Take a function that calls the plugin, and turn it into an SDKv3-compatible credential provider. + * + * What we will do is the following: + * + * - Query the plugin and see what kind of result it gives us. + * - If the result is self-refreshing or doesn't need refreshing, we turn it into an SDKv3 provider + * and return it directly. + * * If the underlying return value is a provider, we will make it a caching provider + * (because we can't know if it will cache by itself or not). + * * If the underlying return value is a static credential, caching isn't relevant. + * * If the underlying return value is V2 credentials, those have caching built-in. + * - If the result is a static credential that expires, we will wrap it in an SDKv3 provider + * that will query the plugin again when the credential expires. + */ +async function v3ProviderFromPlugin(producer: () => Promise): Promise { + const initial = await producer(); + + if (isV3Provider(initial)) { + // Already a provider, make caching + return makeCachingProvider(initial); + } else if (isV3Credentials(initial) && initial.expiration === undefined) { + // Static credentials that don't need refreshing nor caching + return () => Promise.resolve(initial); + } else if (isV3Credentials(initial) && initial.expiration !== undefined) { + // Static credentials that do need refreshing and caching + return refreshFromPluginProvider(initial, producer); + } else if (isV2Credentials(initial)) { + // V2 credentials that refresh and cache themselves + return v3ProviderFromV2Credentials(initial); + } else { + throw new AuthenticationError(`Plugin returned a value that doesn't resemble AWS credentials: ${inspect(initial)}`); + } +} + +/** + * Converts a V2 credential into a V3-compatible provider + */ +function v3ProviderFromV2Credentials(x: SDKv2CompatibleCredentials): AwsCredentialIdentityProvider { + return async () => { + // Get will fetch or refresh as necessary + await x.getPromise(); + + return { + accessKeyId: x.accessKeyId, + secretAccessKey: x.secretAccessKey, + sessionToken: x.sessionToken, + expiration: x.expireTime ?? undefined, + }; + }; +} + +function refreshFromPluginProvider(current: AwsCredentialIdentity, producer: () => Promise): AwsCredentialIdentityProvider { + return async () => { + info(format(current), Date.now()); + if (credentialsAboutToExpire(current)) { + const newCreds = await producer(); + if (!isV3Credentials(newCreds)) { + throw new AuthenticationError(`Plugin initially returned static V3 credentials but now returned something else: ${inspect(newCreds)}`); + } + current = newCreds; + } + return current; + }; +} + +function isV3Provider(x: PluginProviderResult): x is SDKv3CompatibleCredentialProvider { + return typeof x === 'function'; +} + +function isV2Credentials(x: PluginProviderResult): x is SDKv2CompatibleCredentials { + return !!(x && typeof x === 'object' && (x as SDKv2CompatibleCredentials).getPromise); +} + +function isV3Credentials(x: PluginProviderResult): x is SDKv3CompatibleCredentials { + return !!(x && typeof x === 'object' && x.accessKeyId && !isV2Credentials(x)); +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/index.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/index.ts new file mode 100644 index 0000000000000..987c8b1b3da54 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/index.ts @@ -0,0 +1,2 @@ +export * from './sdk'; +export * from './sdk-provider'; diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/provider-caching.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/provider-caching.ts new file mode 100644 index 0000000000000..77b7630230c22 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/provider-caching.ts @@ -0,0 +1,26 @@ +import { memoize } from '@smithy/property-provider'; +import { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@smithy/types'; + +/** + * Wrap a credential provider in a cache + * + * Some credential providers in the SDKv3 are cached (the default Node + * chain, specifically) but most others are not. + * + * Since we want to avoid duplicate calls to `AssumeRole`, or duplicate + * MFA prompts or what have you, we are going to liberally wrap providers + * in caches which will return the cached value until it expires. + */ +export function makeCachingProvider(provider: AwsCredentialIdentityProvider): AwsCredentialIdentityProvider { + return memoize( + provider, + credentialsAboutToExpire, + (token) => !!token.expiration, + ); +} + +export function credentialsAboutToExpire(token: AwsCredentialIdentity) { + const expiryMarginSecs = 5; + // token.expiration is sometimes null + return !!token.expiration && token.expiration.getTime() - Date.now() < expiryMarginSecs * 1000; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk-logger.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk-logger.ts new file mode 100644 index 0000000000000..c13b025392ca8 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk-logger.ts @@ -0,0 +1,142 @@ +import { inspect } from 'util'; +import { Logger } from '@smithy/types'; +import { trace } from '../logging'; + +export class SdkToCliLogger implements Logger { + public trace(..._content: any[]) { + // This is too much detail for our logs + // trace('[SDK trace] %s', fmtContent(content)); + } + + public debug(..._content: any[]) { + // This is too much detail for our logs + // trace('[SDK debug] %s', fmtContent(content)); + } + + /** + * Info is called mostly (exclusively?) for successful API calls + * + * Payload: + * + * (Note the input contains entire CFN templates, for example) + * + * ``` + * { + * clientName: 'S3Client', + * commandName: 'GetBucketLocationCommand', + * input: { + * Bucket: '.....', + * ExpectedBucketOwner: undefined + * }, + * output: { LocationConstraint: 'eu-central-1' }, + * metadata: { + * httpStatusCode: 200, + * requestId: '....', + * extendedRequestId: '...', + * cfId: undefined, + * attempts: 1, + * totalRetryDelay: 0 + * } + * } + * ``` + */ + public info(...content: any[]) { + trace('[sdk info] %s', formatSdkLoggerContent(content)); + } + + public warn(...content: any[]) { + trace('[sdk warn] %s', formatSdkLoggerContent(content)); + } + + /** + * Error is called mostly (exclusively?) for failing API calls + * + * Payload (input would be the entire API call arguments). + * + * ``` + * { + * clientName: 'STSClient', + * commandName: 'GetCallerIdentityCommand', + * input: {}, + * error: AggregateError [ECONNREFUSED]: + * at internalConnectMultiple (node:net:1121:18) + * at afterConnectMultiple (node:net:1688:7) { + * code: 'ECONNREFUSED', + * '$metadata': { attempts: 3, totalRetryDelay: 600 }, + * [errors]: [ [Error], [Error] ] + * }, + * metadata: { attempts: 3, totalRetryDelay: 600 } + * } + * ``` + */ + public error(...content: any[]) { + trace('[sdk error] %s', formatSdkLoggerContent(content)); + } +} + +/** + * This can be anything. + * + * For debug, it seems to be mostly strings. + * For info, it seems to be objects. + * + * Stringify and join without separator. + */ +export function formatSdkLoggerContent(content: any[]) { + if (content.length === 1) { + const apiFmt = formatApiCall(content[0]); + if (apiFmt) { + return apiFmt; + } + } + return content.map((x) => typeof x === 'string' ? x : inspect(x)).join(''); +} + +function formatApiCall(content: any): string | undefined { + if (!isSdkApiCallSuccess(content) && !isSdkApiCallError(content)) { + return undefined; + } + + const service = content.clientName.replace(/Client$/, ''); + const api = content.commandName.replace(/Command$/, ''); + + const parts = []; + if ((content.metadata?.attempts ?? 0) > 1) { + parts.push(`[${content.metadata?.attempts} attempts, ${content.metadata?.totalRetryDelay}ms retry]`); + } + + parts.push(`${service}.${api}(${JSON.stringify(content.input)})`); + + if (isSdkApiCallSuccess(content)) { + parts.push('-> OK'); + } else { + parts.push(`-> ${content.error}`); + } + + return parts.join(' '); +} + +interface SdkApiCallBase { + clientName: string; + commandName: string; + input: Record; + metadata?: { + httpStatusCode?: number; + requestId?: string; + extendedRequestId?: string; + cfId?: string; + attempts?: number; + totalRetryDelay?: number; + }; +} + +type SdkApiCallSuccess = SdkApiCallBase & { output: Record }; +type SdkApiCallError = SdkApiCallBase & { error: Error }; + +function isSdkApiCallSuccess(x: any): x is SdkApiCallSuccess { + return x && typeof x === 'object' && x.commandName && x.output; +} + +function isSdkApiCallError(x: any): x is SdkApiCallError { + return x && typeof x === 'object' && x.commandName && x.error; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk-provider.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk-provider.ts new file mode 100644 index 0000000000000..e4433fe640d2d --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk-provider.ts @@ -0,0 +1,531 @@ +import * as os from 'os'; +import { ContextLookupRoleOptions } from '@aws-cdk/cloud-assembly-schema'; +import { Environment, EnvironmentUtils, UNKNOWN_ACCOUNT, UNKNOWN_REGION } from '@aws-cdk/cx-api'; +import { AssumeRoleCommandInput } from '@aws-sdk/client-sts'; +import { fromTemporaryCredentials } from '@aws-sdk/credential-providers'; +import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler'; +import { AwsCredentialIdentityProvider, Logger } from '@smithy/types'; +import { AwsCliCompatible } from './awscli-compatible'; +import { cached } from './cached'; +import { CredentialPlugins } from './credential-plugins'; +import { makeCachingProvider } from './provider-caching'; +import { SDK } from './sdk'; +import { AuthenticationError } from '../../toolkit/error'; +import { formatErrorMessage } from '../../util/error'; +import { traceMethods } from '../../util/tracing'; +import { debug, warning } from '../logging'; +import { Mode } from '../plugin/mode'; + +export type AssumeRoleAdditionalOptions = Partial>; + +/** + * Options for the default SDK provider + */ +export interface SdkProviderOptions { + /** + * Profile to read from ~/.aws + * + * @default - No profile + */ + readonly profile?: string; + + /** + * HTTP options for SDK + */ + readonly httpOptions?: SdkHttpOptions; + + /** + * The logger for sdk calls. + */ + readonly logger?: Logger; +} + +/** + * Options for individual SDKs + */ +export interface SdkHttpOptions { + /** + * Proxy address to use + * + * @default No proxy + */ + readonly proxyAddress?: string; + + /** + * A path to a certificate bundle that contains a cert to be trusted. + * + * @default No certificate bundle + */ + readonly caBundlePath?: string; +} + +const CACHED_ACCOUNT = Symbol('cached_account'); + +/** + * SDK configuration for a given environment + * 'forEnvironment' will attempt to assume a role and if it + * is not successful, then it will either: + * 1. Check to see if the default credentials (local credentials the CLI was executed with) + * are for the given environment. If they are then return those. + * 2. If the default credentials are not for the given environment then + * throw an error + * + * 'didAssumeRole' allows callers to whether they are receiving the assume role + * credentials or the default credentials. + */ +export interface SdkForEnvironment { + /** + * The SDK for the given environment + */ + readonly sdk: SDK; + + /** + * Whether or not the assume role was successful. + * If the assume role was not successful (false) + * then that means that the 'sdk' returned contains + * the default credentials (not the assume role credentials) + */ + readonly didAssumeRole: boolean; +} + +/** + * Creates instances of the AWS SDK appropriate for a given account/region. + * + * Behavior is as follows: + * + * - First, a set of "base" credentials are established + * - If a target environment is given and the default ("current") SDK credentials are for + * that account, return those; otherwise + * - If a target environment is given, scan all credential provider plugins + * for credentials, and return those if found; otherwise + * - Return default ("current") SDK credentials, noting that they might be wrong. + * + * - Second, a role may optionally need to be assumed. Use the base credentials + * established in the previous process to assume that role. + * - If assuming the role fails and the base credentials are for the correct + * account, return those. This is a fallback for people who are trying to interact + * with a Default Synthesized stack and already have right credentials setup. + * + * Typical cases we see in the wild: + * - Credential plugin setup that, although not recommended, works for them + * - Seeded terminal with `ReadOnly` credentials in order to do `cdk diff`--the `ReadOnly` + * role doesn't have `sts:AssumeRole` and will fail for no real good reason. + */ +@traceMethods +export class SdkProvider { + /** + * Create a new SdkProvider which gets its defaults in a way that behaves like the AWS CLI does + * + * The AWS SDK for JS behaves slightly differently from the AWS CLI in a number of ways; see the + * class `AwsCliCompatible` for the details. + */ + public static async withAwsCliCompatibleDefaults(options: SdkProviderOptions = {}) { + const credentialProvider = await AwsCliCompatible.credentialChainBuilder({ + profile: options.profile, + httpOptions: options.httpOptions, + logger: options.logger, + }); + + const region = await AwsCliCompatible.region(options.profile); + const requestHandler = AwsCliCompatible.requestHandlerBuilder(options.httpOptions); + return new SdkProvider(credentialProvider, region, requestHandler, options.logger); + } + + private readonly plugins = new CredentialPlugins(); + + public constructor( + private readonly defaultCredentialProvider: AwsCredentialIdentityProvider, + /** + * Default region + */ + public readonly defaultRegion: string, + private readonly requestHandler: NodeHttpHandlerOptions = {}, + private readonly logger?: Logger, + ) {} + + /** + * Return an SDK which can do operations in the given environment + * + * The `environment` parameter is resolved first (see `resolveEnvironment()`). + */ + public async forEnvironment( + environment: Environment, + mode: Mode, + options?: CredentialsOptions, + quiet = false, + ): Promise { + const env = await this.resolveEnvironment(environment); + + const baseCreds = await this.obtainBaseCredentials(env.account, mode); + + // At this point, we need at least SOME credentials + if (baseCreds.source === 'none') { + throw new AuthenticationError(fmtObtainCredentialsError(env.account, baseCreds)); + } + + // Simple case is if we don't need to "assumeRole" here. If so, we must now have credentials for the right + // account. + if (options?.assumeRoleArn === undefined) { + if (baseCreds.source === 'incorrectDefault') { + throw new AuthenticationError(fmtObtainCredentialsError(env.account, baseCreds)); + } + + // Our current credentials must be valid and not expired. Confirm that before we get into doing + // actual CloudFormation calls, which might take a long time to hang. + const sdk = new SDK(baseCreds.credentials, env.region, this.requestHandler, this.logger); + await sdk.validateCredentials(); + return { sdk, didAssumeRole: false }; + } + + try { + // We will proceed to AssumeRole using whatever we've been given. + const sdk = await this.withAssumedRole( + baseCreds, + options.assumeRoleArn, + options.assumeRoleExternalId, + options.assumeRoleAdditionalOptions, + env.region, + ); + + return { sdk, didAssumeRole: true }; + } catch (err: any) { + if (err.name === 'ExpiredToken') { + throw err; + } + + // AssumeRole failed. Proceed and warn *if and only if* the baseCredentials were already for the right account + // or returned from a plugin. This is to cover some current setups for people using plugins or preferring to + // feed the CLI credentials which are sufficient by themselves. Prefer to assume the correct role if we can, + // but if we can't then let's just try with available credentials anyway. + if (baseCreds.source === 'correctDefault' || baseCreds.source === 'plugin') { + debug(err.message); + 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.requestHandler, this.logger), + didAssumeRole: false, + }; + } + + throw err; + } + } + + /** + * Return the partition that base credentials are for + * + * Returns `undefined` if there are no base credentials. + */ + public async baseCredentialsPartition(environment: Environment, mode: Mode): Promise { + const env = await this.resolveEnvironment(environment); + const baseCreds = await this.obtainBaseCredentials(env.account, mode); + if (baseCreds.source === 'none') { + return undefined; + } + return (await new SDK(baseCreds.credentials, env.region, this.requestHandler, this.logger).currentAccount()).partition; + } + + /** + * Resolve the environment for a stack + * + * Replaces the magic values `UNKNOWN_REGION` and `UNKNOWN_ACCOUNT` + * with the defaults for the current SDK configuration (`~/.aws/config` or + * otherwise). + * + * It is an error if `UNKNOWN_ACCOUNT` is used but the user hasn't configured + * any SDK credentials. + */ + public async resolveEnvironment(env: Environment): Promise { + const region = env.region !== UNKNOWN_REGION ? env.region : this.defaultRegion; + const account = env.account !== UNKNOWN_ACCOUNT ? env.account : (await this.defaultAccount())?.accountId; + + if (!account) { + throw new AuthenticationError( + 'Unable to resolve AWS account to use. It must be either configured when you define your CDK Stack, or through the environment', + ); + } + + return { + region, + account, + name: EnvironmentUtils.format(account, region), + }; + } + + /** + * The account we'd auth into if we used default credentials. + * + * Default credentials are the set of ambiently configured credentials using + * one of the environment variables, or ~/.aws/credentials, or the *one* + * profile that was passed into the CLI. + * + * Might return undefined if there are no default/ambient credentials + * available (in which case the user should better hope they have + * credential plugins configured). + * + * Uses a cache to avoid STS calls if we don't need 'em. + */ + public async defaultAccount(): Promise { + return cached(this, CACHED_ACCOUNT, async () => { + try { + return await new SDK(this.defaultCredentialProvider, this.defaultRegion, this.requestHandler, this.logger).currentAccount(); + } catch (e: any) { + // Treat 'ExpiredToken' specially. This is a common situation that people may find themselves in, and + // they are complaining about if we fail 'cdk synth' on them. We loudly complain in order to show that + // the current situation is probably undesirable, but we don't fail. + if (e.name === 'ExpiredToken') { + warning( + 'There are expired AWS credentials in your environment. The CDK app will synth without current account information.', + ); + return undefined; + } + + debug(`Unable to determine the default AWS account (${e.name}): ${formatErrorMessage(e)}`); + return undefined; + } + }); + } + + /** + * Get credentials for the given account ID in the given mode + * + * 1. Use the default credentials if the destination account matches the + * current credentials' account. + * 2. Otherwise try all credential plugins. + * 3. Fail if neither of these yield any credentials. + * 4. Return a failure if any of them returned credentials + */ + private async obtainBaseCredentials(accountId: string, mode: Mode): Promise { + // First try 'current' credentials + const defaultAccountId = (await this.defaultAccount())?.accountId; + if (defaultAccountId === accountId) { + return { + source: 'correctDefault', + credentials: await this.defaultCredentialProvider, + }; + } + + // Then try the plugins + const pluginCreds = await this.plugins.fetchCredentialsFor(accountId, mode); + if (pluginCreds) { + return { source: 'plugin', ...pluginCreds }; + } + + // Fall back to default credentials with a note that they're not the right ones yet + if (defaultAccountId !== undefined) { + return { + source: 'incorrectDefault', + accountId: defaultAccountId, + credentials: await this.defaultCredentialProvider, + unusedPlugins: this.plugins.availablePluginNames, + }; + } + + // Apparently we didn't find any at all + return { + source: 'none', + unusedPlugins: this.plugins.availablePluginNames, + }; + } + + /** + * Return an SDK which uses assumed role credentials + * + * The base credentials used to retrieve the assumed role credentials will be the + * same credentials returned by obtainCredentials if an environment and mode is passed, + * otherwise it will be the current credentials. + */ + private async withAssumedRole( + mainCredentials: Exclude, + roleArn: string, + externalId?: string, + additionalOptions?: AssumeRoleAdditionalOptions, + region?: string, + ): Promise { + debug(`Assuming role '${roleArn}'.`); + + region = region ?? this.defaultRegion; + + const sourceDescription = fmtObtainedCredentials(mainCredentials); + + try { + const credentials = await makeCachingProvider(fromTemporaryCredentials({ + masterCredentials: mainCredentials.credentials, + params: { + RoleArn: roleArn, + ExternalId: externalId, + RoleSessionName: `aws-cdk-${safeUsername()}`, + ...additionalOptions, + TransitiveTagKeys: additionalOptions?.Tags ? additionalOptions.Tags.map((t) => t.Key!) : undefined, + }, + clientConfig: { + region, + requestHandler: this.requestHandler, + customUserAgent: 'aws-cdk', + logger: this.logger, + }, + logger: this.logger, + })); + + // Call the provider at least once here, to catch an error if it occurs + await credentials(); + + return new SDK(credentials, region, this.requestHandler, this.logger); + } catch (err: any) { + if (err.name === 'ExpiredToken') { + throw err; + } + + debug(`Assuming role failed: ${err.message}`); + throw new AuthenticationError( + [ + 'Could not assume role in target account', + ...(sourceDescription ? [`using ${sourceDescription}`] : []), + err.message, + ". Please make sure that this role exists in the account. If it doesn't exist, (re)-bootstrap the environment " + + "with the right '--trust', using the latest version of the CDK CLI.", + ].join(' '), + ); + } + } +} + +/** + * An AWS account + * + * An AWS account always exists in only one partition. Usually we don't care about + * the partition, but when we need to form ARNs we do. + */ +export interface Account { + /** + * The account number + */ + readonly accountId: string; + + /** + * The partition ('aws' or 'aws-cn' or otherwise) + */ + readonly partition: string; +} + +/** + * Return the username with characters invalid for a RoleSessionName removed + * + * @see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html#API_AssumeRole_RequestParameters + */ +function safeUsername() { + try { + return os.userInfo().username.replace(/[^\w+=,.@-]/g, '@'); + } catch { + return 'noname'; + } +} + +/** + * Options for obtaining credentials for an environment + */ +export interface CredentialsOptions { + /** + * The ARN of the role that needs to be assumed, if any + */ + readonly assumeRoleArn?: string; + + /** + * External ID required to assume the given role. + */ + readonly assumeRoleExternalId?: string; + + /** + * Session tags required to assume the given role. + */ + readonly assumeRoleAdditionalOptions?: AssumeRoleAdditionalOptions; +} + +/** + * Result of obtaining base credentials + */ +type ObtainBaseCredentialsResult = + | { source: 'correctDefault'; credentials: AwsCredentialIdentityProvider } + | { source: 'plugin'; pluginName: string; credentials: AwsCredentialIdentityProvider } + | { + source: 'incorrectDefault'; + credentials: AwsCredentialIdentityProvider; + accountId: string; + unusedPlugins: string[]; + } + | { source: 'none'; unusedPlugins: string[] }; + +/** + * Isolating the code that translates calculation errors into human error messages + * + * We cover the following cases: + * + * - No credentials are available at all + * - Default credentials are for the wrong account + */ +function fmtObtainCredentialsError( + targetAccountId: string, + obtainResult: ObtainBaseCredentialsResult & { + source: 'none' | 'incorrectDefault'; + }, +): string { + const msg = [`Need to perform AWS calls for account ${targetAccountId}`]; + switch (obtainResult.source) { + case 'incorrectDefault': + msg.push(`but the current credentials are for ${obtainResult.accountId}`); + break; + case 'none': + msg.push('but no credentials have been configured'); + } + if (obtainResult.unusedPlugins.length > 0) { + msg.push(`and none of these plugins found any: ${obtainResult.unusedPlugins.join(', ')}`); + } + return msg.join(', '); +} + +/** + * Format a message indicating where we got base credentials for the assume role + * + * We cover the following cases: + * + * - Default credentials for the right account + * - Default credentials for the wrong account + * - Credentials returned from a plugin + */ +function fmtObtainedCredentials(obtainResult: Exclude): string { + switch (obtainResult.source) { + case 'correctDefault': + return 'current credentials'; + case 'plugin': + return `credentials returned by plugin '${obtainResult.pluginName}'`; + case 'incorrectDefault': + const msg = []; + msg.push(`current credentials (which are for account ${obtainResult.accountId}`); + + if (obtainResult.unusedPlugins.length > 0) { + msg.push(`, and none of the following plugins provided credentials: ${obtainResult.unusedPlugins.join(', ')}`); + } + msg.push(')'); + + return msg.join(''); + } +} + +/** + * Instantiate an SDK for context providers. This function ensures that all + * lookup assume role options are used when context providers perform lookups. + */ +export async function initContextProviderSdk(aws: SdkProvider, options: ContextLookupRoleOptions): Promise { + const account = options.account; + const region = options.region; + + const creds: CredentialsOptions = { + assumeRoleArn: options.lookupRoleArn, + assumeRoleExternalId: options.lookupRoleExternalId, + assumeRoleAdditionalOptions: options.assumeRoleAdditionalOptions, + }; + + return (await aws.forEnvironment(EnvironmentUtils.make(account, region), Mode.ForReading, creds)).sdk; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk.ts new file mode 100644 index 0000000000000..663e1c4f8890d --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/sdk.ts @@ -0,0 +1,987 @@ +import { + AppSyncClient, + FunctionConfiguration, + GetSchemaCreationStatusCommand, + type GetSchemaCreationStatusCommandInput, + type GetSchemaCreationStatusCommandOutput, + type ListFunctionsCommandInput, + paginateListFunctions, + StartSchemaCreationCommand, + type StartSchemaCreationCommandInput, + type StartSchemaCreationCommandOutput, + UpdateApiKeyCommand, + type UpdateApiKeyCommandInput, + type UpdateApiKeyCommandOutput, + UpdateFunctionCommand, + type UpdateFunctionCommandInput, + type UpdateFunctionCommandOutput, + UpdateResolverCommand, + type UpdateResolverCommandInput, + type UpdateResolverCommandOutput, +} from '@aws-sdk/client-appsync'; +import { + CloudFormationClient, + ContinueUpdateRollbackCommand, + ContinueUpdateRollbackCommandInput, + ContinueUpdateRollbackCommandOutput, + CreateChangeSetCommand, + type CreateChangeSetCommandInput, + type CreateChangeSetCommandOutput, + CreateGeneratedTemplateCommand, + type CreateGeneratedTemplateCommandInput, + type CreateGeneratedTemplateCommandOutput, + CreateStackCommand, + type CreateStackCommandInput, + type CreateStackCommandOutput, + DeleteChangeSetCommand, + type DeleteChangeSetCommandInput, + type DeleteChangeSetCommandOutput, + DeleteGeneratedTemplateCommand, + type DeleteGeneratedTemplateCommandInput, + type DeleteGeneratedTemplateCommandOutput, + DeleteStackCommand, + type DeleteStackCommandInput, + type DeleteStackCommandOutput, + DescribeChangeSetCommand, + type DescribeChangeSetCommandInput, + type DescribeChangeSetCommandOutput, + DescribeGeneratedTemplateCommand, + type DescribeGeneratedTemplateCommandInput, + type DescribeGeneratedTemplateCommandOutput, + DescribeResourceScanCommand, + type DescribeResourceScanCommandInput, + type DescribeResourceScanCommandOutput, + DescribeStackEventsCommand, + type DescribeStackEventsCommandInput, + DescribeStackEventsCommandOutput, + DescribeStackResourcesCommand, + DescribeStackResourcesCommandInput, + DescribeStackResourcesCommandOutput, + DescribeStacksCommand, + type DescribeStacksCommandInput, + type DescribeStacksCommandOutput, + ExecuteChangeSetCommand, + type ExecuteChangeSetCommandInput, + type ExecuteChangeSetCommandOutput, + GetGeneratedTemplateCommand, + type GetGeneratedTemplateCommandInput, + type GetGeneratedTemplateCommandOutput, + GetTemplateCommand, + type GetTemplateCommandInput, + type GetTemplateCommandOutput, + GetTemplateSummaryCommand, + type GetTemplateSummaryCommandInput, + type GetTemplateSummaryCommandOutput, + ListExportsCommand, + type ListExportsCommandInput, + type ListExportsCommandOutput, + ListResourceScanRelatedResourcesCommand, + type ListResourceScanRelatedResourcesCommandInput, + type ListResourceScanRelatedResourcesCommandOutput, + ListResourceScanResourcesCommand, + type ListResourceScanResourcesCommandInput, + type ListResourceScanResourcesCommandOutput, + ListResourceScansCommand, + type ListResourceScansCommandInput, + type ListResourceScansCommandOutput, + type ListStackResourcesCommandInput, + ListStacksCommand, + ListStacksCommandInput, + ListStacksCommandOutput, + paginateListStackResources, + RollbackStackCommand, + RollbackStackCommandInput, + RollbackStackCommandOutput, + StackResourceSummary, + StartResourceScanCommand, + type StartResourceScanCommandInput, + type StartResourceScanCommandOutput, + UpdateStackCommand, + type UpdateStackCommandInput, + type UpdateStackCommandOutput, + UpdateTerminationProtectionCommand, + type UpdateTerminationProtectionCommandInput, + type UpdateTerminationProtectionCommandOutput, +} from '@aws-sdk/client-cloudformation'; +import { + CloudWatchLogsClient, + DescribeLogGroupsCommand, + type DescribeLogGroupsCommandInput, + type DescribeLogGroupsCommandOutput, + FilterLogEventsCommand, + FilterLogEventsCommandInput, + FilterLogEventsCommandOutput, +} from '@aws-sdk/client-cloudwatch-logs'; +import { + CodeBuildClient, + UpdateProjectCommand, + type UpdateProjectCommandInput, + type UpdateProjectCommandOutput, +} from '@aws-sdk/client-codebuild'; +import { + DescribeAvailabilityZonesCommand, + type DescribeAvailabilityZonesCommandInput, + type DescribeAvailabilityZonesCommandOutput, + DescribeImagesCommand, + type DescribeImagesCommandInput, + type DescribeImagesCommandOutput, + DescribeInstancesCommand, + type DescribeInstancesCommandInput, + type DescribeInstancesCommandOutput, + DescribeRouteTablesCommand, + type DescribeRouteTablesCommandInput, + type DescribeRouteTablesCommandOutput, + DescribeSecurityGroupsCommand, + type DescribeSecurityGroupsCommandInput, + type DescribeSecurityGroupsCommandOutput, + DescribeSubnetsCommand, + type DescribeSubnetsCommandInput, + type DescribeSubnetsCommandOutput, + DescribeVpcEndpointServicesCommand, + type DescribeVpcEndpointServicesCommandInput, + type DescribeVpcEndpointServicesCommandOutput, + DescribeVpcsCommand, + type DescribeVpcsCommandInput, + type DescribeVpcsCommandOutput, + DescribeVpnGatewaysCommand, + type DescribeVpnGatewaysCommandInput, + type DescribeVpnGatewaysCommandOutput, + EC2Client, +} from '@aws-sdk/client-ec2'; +import { + BatchDeleteImageCommand, + BatchDeleteImageCommandInput, + BatchDeleteImageCommandOutput, + CreateRepositoryCommand, + type CreateRepositoryCommandInput, + type CreateRepositoryCommandOutput, + DescribeImagesCommand as ECRDescribeImagesCommand, + type DescribeImagesCommandInput as ECRDescribeImagesCommandInput, + type DescribeImagesCommandOutput as ECRDescribeImagesCommandOutput, + DescribeRepositoriesCommand, + type DescribeRepositoriesCommandInput, + type DescribeRepositoriesCommandOutput, + ECRClient, + GetAuthorizationTokenCommand, + type GetAuthorizationTokenCommandInput, + type GetAuthorizationTokenCommandOutput, + ListImagesCommand, + ListImagesCommandInput, + ListImagesCommandOutput, + PutImageCommand, + PutImageCommandInput, + PutImageCommandOutput, + PutImageScanningConfigurationCommand, + type PutImageScanningConfigurationCommandInput, + type PutImageScanningConfigurationCommandOutput, + BatchGetImageCommandInput, + BatchGetImageCommand, + BatchGetImageCommandOutput, +} from '@aws-sdk/client-ecr'; +import { + DescribeServicesCommandInput, + ECSClient, + ListClustersCommand, + type ListClustersCommandInput, + type ListClustersCommandOutput, + RegisterTaskDefinitionCommand, + RegisterTaskDefinitionCommandInput, + type RegisterTaskDefinitionCommandOutput, + UpdateServiceCommand, + type UpdateServiceCommandInput, + type UpdateServiceCommandOutput, + waitUntilServicesStable, +} from '@aws-sdk/client-ecs'; +import { + DescribeListenersCommand, + type DescribeListenersCommandInput, + type DescribeListenersCommandOutput, + DescribeLoadBalancersCommand, + type DescribeLoadBalancersCommandInput, + type DescribeLoadBalancersCommandOutput, + DescribeTagsCommand, + type DescribeTagsCommandInput, + type DescribeTagsCommandOutput, + ElasticLoadBalancingV2Client, + Listener, + LoadBalancer, + paginateDescribeListeners, + paginateDescribeLoadBalancers, +} from '@aws-sdk/client-elastic-load-balancing-v2'; +import { + CreatePolicyCommand, + type CreatePolicyCommandInput, + type CreatePolicyCommandOutput, + GetPolicyCommand, + type GetPolicyCommandInput, + type GetPolicyCommandOutput, + GetRoleCommand, + type GetRoleCommandInput, + type GetRoleCommandOutput, + IAMClient, +} from '@aws-sdk/client-iam'; +import { + DescribeKeyCommand, + type DescribeKeyCommandInput, + type DescribeKeyCommandOutput, + KMSClient, + ListAliasesCommand, + type ListAliasesCommandInput, + type ListAliasesCommandOutput, +} from '@aws-sdk/client-kms'; +import { + InvokeCommand, + type InvokeCommandInput, + type InvokeCommandOutput, + LambdaClient, + PublishVersionCommand, + type PublishVersionCommandInput, + type PublishVersionCommandOutput, + UpdateAliasCommand, + type UpdateAliasCommandInput, + type UpdateAliasCommandOutput, + UpdateFunctionCodeCommand, + type UpdateFunctionCodeCommandInput, + type UpdateFunctionCodeCommandOutput, + UpdateFunctionConfigurationCommand, + type UpdateFunctionConfigurationCommandInput, + type UpdateFunctionConfigurationCommandOutput, + waitUntilFunctionUpdatedV2, +} from '@aws-sdk/client-lambda'; +import { + GetHostedZoneCommand, + type GetHostedZoneCommandInput, + type GetHostedZoneCommandOutput, + ListHostedZonesByNameCommand, + type ListHostedZonesByNameCommandInput, + type ListHostedZonesByNameCommandOutput, + ListHostedZonesCommand, + type ListHostedZonesCommandInput, + type ListHostedZonesCommandOutput, + Route53Client, +} from '@aws-sdk/client-route-53'; +import { + type CompleteMultipartUploadCommandOutput, + DeleteObjectsCommand, + DeleteObjectsCommandInput, + DeleteObjectsCommandOutput, + DeleteObjectTaggingCommand, + DeleteObjectTaggingCommandInput, + DeleteObjectTaggingCommandOutput, + GetBucketEncryptionCommand, + type GetBucketEncryptionCommandInput, + type GetBucketEncryptionCommandOutput, + GetBucketLocationCommand, + type GetBucketLocationCommandInput, + type GetBucketLocationCommandOutput, + GetObjectCommand, + type GetObjectCommandInput, + type GetObjectCommandOutput, + GetObjectTaggingCommand, + GetObjectTaggingCommandInput, + GetObjectTaggingCommandOutput, + ListObjectsV2Command, + type ListObjectsV2CommandInput, + type ListObjectsV2CommandOutput, + type PutObjectCommandInput, + PutObjectTaggingCommand, + PutObjectTaggingCommandInput, + PutObjectTaggingCommandOutput, + S3Client, +} from '@aws-sdk/client-s3'; +import { + GetSecretValueCommand, + type GetSecretValueCommandInput, + type GetSecretValueCommandOutput, + SecretsManagerClient, +} from '@aws-sdk/client-secrets-manager'; +import { + SFNClient, + UpdateStateMachineCommand, + UpdateStateMachineCommandInput, + UpdateStateMachineCommandOutput, +} from '@aws-sdk/client-sfn'; +import { + GetParameterCommand, + type GetParameterCommandInput, + type GetParameterCommandOutput, + SSMClient, +} from '@aws-sdk/client-ssm'; +import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'; +import { Upload } from '@aws-sdk/lib-storage'; +import { getEndpointFromInstructions } from '@smithy/middleware-endpoint'; +import type { NodeHttpHandlerOptions } from '@smithy/node-http-handler'; +import { AwsCredentialIdentityProvider, Logger } from '@smithy/types'; +import { ConfiguredRetryStrategy } from '@smithy/util-retry'; +import { WaiterResult } from '@smithy/util-waiter'; +import { AccountAccessKeyCache } from './account-cache'; +import { cachedAsync } from './cached'; +import { Account } from './sdk-provider'; +import { defaultCliUserAgent } from './user-agent'; +import { AuthenticationError } from '../../toolkit/error'; +import { formatErrorMessage } from '../../util/error'; +import { traceMethods } from '../../util/tracing'; +import { debug } from '../logging'; + +export interface S3ClientOptions { + /** + * If APIs are used that require MD5 checksums. + * + * Some S3 APIs in SDKv2 have a bug that always requires them to use a MD5 checksum. + * These APIs are not going to be supported in a FIPS environment. + */ + needsMd5Checksums?: boolean; +} + +/** + * Additional SDK configuration options + */ +export interface SdkOptions { + /** + * Additional descriptive strings that indicate where the "AssumeRole" credentials are coming from + * + * Will be printed in an error message to help users diagnose auth problems. + */ + readonly assumeRoleCredentialsSourceDescription?: string; +} + +// TODO: still some cleanup here. Make the pagination functions do all the work here instead of in individual packages. +// Also add async/await. Does that actually matter in this context? Find out and update accordingly. + +// Also add notes to the PR about why you imported everything individually and used 'type' so reviewers don't have to ask. + +export interface ConfigurationOptions { + region: string; + credentials: AwsCredentialIdentityProvider; + requestHandler: NodeHttpHandlerOptions; + retryStrategy: ConfiguredRetryStrategy; + customUserAgent: string; + logger?: Logger; + s3DisableBodySigning?: boolean; + computeChecksums?: boolean; +} + +export interface IAppSyncClient { + getSchemaCreationStatus(input: GetSchemaCreationStatusCommandInput): Promise; + startSchemaCreation(input: StartSchemaCreationCommandInput): Promise; + updateApiKey(input: UpdateApiKeyCommandInput): Promise; + updateFunction(input: UpdateFunctionCommandInput): Promise; + updateResolver(input: UpdateResolverCommandInput): Promise; + // Pagination functions + listFunctions(input: ListFunctionsCommandInput): Promise; +} + +export interface ICloudFormationClient { + continueUpdateRollback(input: ContinueUpdateRollbackCommandInput): Promise; + createChangeSet(input: CreateChangeSetCommandInput): Promise; + createGeneratedTemplate(input: CreateGeneratedTemplateCommandInput): Promise; + createStack(input: CreateStackCommandInput): Promise; + deleteChangeSet(input: DeleteChangeSetCommandInput): Promise; + deleteGeneratedTemplate(input: DeleteGeneratedTemplateCommandInput): Promise; + deleteStack(input: DeleteStackCommandInput): Promise; + describeChangeSet(input: DescribeChangeSetCommandInput): Promise; + describeGeneratedTemplate( + input: DescribeGeneratedTemplateCommandInput, + ): Promise; + describeResourceScan(input: DescribeResourceScanCommandInput): Promise; + describeStacks(input: DescribeStacksCommandInput): Promise; + describeStackResources(input: DescribeStackResourcesCommandInput): Promise; + executeChangeSet(input: ExecuteChangeSetCommandInput): Promise; + getGeneratedTemplate(input: GetGeneratedTemplateCommandInput): Promise; + getTemplate(input: GetTemplateCommandInput): Promise; + getTemplateSummary(input: GetTemplateSummaryCommandInput): Promise; + listExports(input: ListExportsCommandInput): Promise; + listResourceScanRelatedResources( + input: ListResourceScanRelatedResourcesCommandInput, + ): Promise; + listResourceScanResources( + input: ListResourceScanResourcesCommandInput, + ): Promise; + listResourceScans(input?: ListResourceScansCommandInput): Promise; + listStacks(input: ListStacksCommandInput): Promise; + rollbackStack(input: RollbackStackCommandInput): Promise; + startResourceScan(input: StartResourceScanCommandInput): Promise; + updateStack(input: UpdateStackCommandInput): Promise; + updateTerminationProtection( + input: UpdateTerminationProtectionCommandInput, + ): Promise; + // Pagination functions + describeStackEvents(input: DescribeStackEventsCommandInput): Promise; + listStackResources(input: ListStackResourcesCommandInput): Promise; +} + +export interface ICloudWatchLogsClient { + describeLogGroups(input: DescribeLogGroupsCommandInput): Promise; + filterLogEvents(input: FilterLogEventsCommandInput): Promise; +} + +export interface ICodeBuildClient { + updateProject(input: UpdateProjectCommandInput): Promise; +} +export interface IEC2Client { + describeAvailabilityZones( + input: DescribeAvailabilityZonesCommandInput, + ): Promise; + describeImages(input: DescribeImagesCommandInput): Promise; + describeInstances(input: DescribeInstancesCommandInput): Promise; + describeRouteTables(input: DescribeRouteTablesCommandInput): Promise; + describeSecurityGroups(input: DescribeSecurityGroupsCommandInput): Promise; + describeSubnets(input: DescribeSubnetsCommandInput): Promise; + describeVpcEndpointServices( + input: DescribeVpcEndpointServicesCommandInput, + ): Promise; + describeVpcs(input: DescribeVpcsCommandInput): Promise; + describeVpnGateways(input: DescribeVpnGatewaysCommandInput): Promise; +} + +export interface IECRClient { + batchDeleteImage(input: BatchDeleteImageCommandInput): Promise; + batchGetImage(input: BatchGetImageCommandInput): Promise; + createRepository(input: CreateRepositoryCommandInput): Promise; + describeImages(input: ECRDescribeImagesCommandInput): Promise; + describeRepositories(input: DescribeRepositoriesCommandInput): Promise; + getAuthorizationToken(input: GetAuthorizationTokenCommandInput): Promise; + listImages(input: ListImagesCommandInput): Promise; + putImage(input: PutImageCommandInput): Promise; + putImageScanningConfiguration( + input: PutImageScanningConfigurationCommandInput, + ): Promise; +} + +export interface IECSClient { + listClusters(input: ListClustersCommandInput): Promise; + registerTaskDefinition(input: RegisterTaskDefinitionCommandInput): Promise; + updateService(input: UpdateServiceCommandInput): Promise; + // Waiters + waitUntilServicesStable(input: DescribeServicesCommandInput): Promise; +} + +export interface IElasticLoadBalancingV2Client { + describeListeners(input: DescribeListenersCommandInput): Promise; + describeLoadBalancers(input: DescribeLoadBalancersCommandInput): Promise; + describeTags(input: DescribeTagsCommandInput): Promise; + // Pagination + paginateDescribeListeners(input: DescribeListenersCommandInput): Promise; + paginateDescribeLoadBalancers(input: DescribeLoadBalancersCommandInput): Promise; +} + +export interface IIAMClient { + createPolicy(input: CreatePolicyCommandInput): Promise; + getPolicy(input: GetPolicyCommandInput): Promise; + getRole(input: GetRoleCommandInput): Promise; +} + +export interface IKMSClient { + describeKey(input: DescribeKeyCommandInput): Promise; + listAliases(input: ListAliasesCommandInput): Promise; +} + +export interface ILambdaClient { + invokeCommand(input: InvokeCommandInput): Promise; + publishVersion(input: PublishVersionCommandInput): Promise; + updateAlias(input: UpdateAliasCommandInput): Promise; + updateFunctionCode(input: UpdateFunctionCodeCommandInput): Promise; + updateFunctionConfiguration( + input: UpdateFunctionConfigurationCommandInput, + ): Promise; + // Waiters + waitUntilFunctionUpdated(delaySeconds: number, input: UpdateFunctionConfigurationCommandInput): Promise; +} + +export interface IRoute53Client { + getHostedZone(input: GetHostedZoneCommandInput): Promise; + listHostedZones(input: ListHostedZonesCommandInput): Promise; + listHostedZonesByName(input: ListHostedZonesByNameCommandInput): Promise; +} + +export interface IS3Client { + deleteObjects(input: DeleteObjectsCommandInput): Promise; + deleteObjectTagging(input: DeleteObjectTaggingCommandInput): Promise; + getBucketEncryption(input: GetBucketEncryptionCommandInput): Promise; + getBucketLocation(input: GetBucketLocationCommandInput): Promise; + getObject(input: GetObjectCommandInput): Promise; + getObjectTagging(input: GetObjectTaggingCommandInput): Promise; + listObjectsV2(input: ListObjectsV2CommandInput): Promise; + putObjectTagging(input: PutObjectTaggingCommandInput): Promise; + upload(input: PutObjectCommandInput): Promise; +} + +export interface ISecretsManagerClient { + getSecretValue(input: GetSecretValueCommandInput): Promise; +} + +export interface ISSMClient { + getParameter(input: GetParameterCommandInput): Promise; +} + +export interface IStepFunctionsClient { + updateStateMachine(input: UpdateStateMachineCommandInput): Promise; +} + +/** + * Base functionality of SDK without credential fetching + */ +@traceMethods +export class SDK { + private static readonly accountCache = new AccountAccessKeyCache(); + + public readonly currentRegion: string; + + public readonly config: ConfigurationOptions; + + /** + * STS is used to check credential validity, don't do too many retries. + */ + private readonly stsRetryStrategy = new ConfiguredRetryStrategy(3, (attempt) => 100 * (2 ** attempt)); + + /** + * Whether we have proof that the credentials have not expired + * + * We need to do some manual plumbing around this because the JS SDKv2 treats `ExpiredToken` + * as retriable and we have hefty retries on CFN calls making the CLI hang for a good 15 minutes + * if the credentials have expired. + */ + private _credentialsValidated = false; + + constructor( + private readonly credProvider: AwsCredentialIdentityProvider, + region: string, + requestHandler: NodeHttpHandlerOptions, + logger?: Logger, + ) { + this.config = { + region, + credentials: credProvider, + requestHandler, + retryStrategy: new ConfiguredRetryStrategy(7, (attempt) => 300 * (2 ** attempt)), + customUserAgent: defaultCliUserAgent(), + logger, + }; + this.currentRegion = region; + } + + public appendCustomUserAgent(userAgentData?: string): void { + if (!userAgentData) { + return; + } + + const currentCustomUserAgent = this.config.customUserAgent; + this.config.customUserAgent = currentCustomUserAgent ? `${currentCustomUserAgent} ${userAgentData}` : userAgentData; + } + + public removeCustomUserAgent(userAgentData: string): void { + this.config.customUserAgent = this.config.customUserAgent?.replace(userAgentData, ''); + } + + public appsync(): IAppSyncClient { + const client = new AppSyncClient(this.config); + return { + getSchemaCreationStatus: ( + input: GetSchemaCreationStatusCommandInput, + ): Promise => client.send(new GetSchemaCreationStatusCommand(input)), + startSchemaCreation: (input: StartSchemaCreationCommandInput): Promise => + client.send(new StartSchemaCreationCommand(input)), + updateApiKey: (input: UpdateApiKeyCommandInput): Promise => + client.send(new UpdateApiKeyCommand(input)), + updateFunction: (input: UpdateFunctionCommandInput): Promise => + client.send(new UpdateFunctionCommand(input)), + updateResolver: (input: UpdateResolverCommandInput): Promise => + client.send(new UpdateResolverCommand(input)), + + // Pagination Functions + listFunctions: async (input: ListFunctionsCommandInput): Promise => { + const functions = Array(); + const paginator = paginateListFunctions({ client }, input); + for await (const page of paginator) { + functions.push(...(page.functions || [])); + } + return functions; + }, + }; + } + + public cloudFormation(): ICloudFormationClient { + const client = new CloudFormationClient({ + ...this.config, + retryStrategy: new ConfiguredRetryStrategy(11, (attempt: number) => 1000 * (2 ** attempt)), + }); + return { + continueUpdateRollback: async ( + input: ContinueUpdateRollbackCommandInput, + ): Promise => client.send(new ContinueUpdateRollbackCommand(input)), + createChangeSet: (input: CreateChangeSetCommandInput): Promise => + client.send(new CreateChangeSetCommand(input)), + createGeneratedTemplate: ( + input: CreateGeneratedTemplateCommandInput, + ): Promise => client.send(new CreateGeneratedTemplateCommand(input)), + createStack: (input: CreateStackCommandInput): Promise => + client.send(new CreateStackCommand(input)), + deleteChangeSet: (input: DeleteChangeSetCommandInput): Promise => + client.send(new DeleteChangeSetCommand(input)), + deleteGeneratedTemplate: ( + input: DeleteGeneratedTemplateCommandInput, + ): Promise => client.send(new DeleteGeneratedTemplateCommand(input)), + deleteStack: (input: DeleteStackCommandInput): Promise => + client.send(new DeleteStackCommand(input)), + describeChangeSet: (input: DescribeChangeSetCommandInput): Promise => + client.send(new DescribeChangeSetCommand(input)), + describeGeneratedTemplate: ( + input: DescribeGeneratedTemplateCommandInput, + ): Promise => client.send(new DescribeGeneratedTemplateCommand(input)), + describeResourceScan: (input: DescribeResourceScanCommandInput): Promise => + client.send(new DescribeResourceScanCommand(input)), + describeStacks: (input: DescribeStacksCommandInput): Promise => + client.send(new DescribeStacksCommand(input)), + describeStackResources: (input: DescribeStackResourcesCommandInput): Promise => + client.send(new DescribeStackResourcesCommand(input)), + executeChangeSet: (input: ExecuteChangeSetCommandInput): Promise => + client.send(new ExecuteChangeSetCommand(input)), + getGeneratedTemplate: (input: GetGeneratedTemplateCommandInput): Promise => + client.send(new GetGeneratedTemplateCommand(input)), + getTemplate: (input: GetTemplateCommandInput): Promise => + client.send(new GetTemplateCommand(input)), + getTemplateSummary: (input: GetTemplateSummaryCommandInput): Promise => + client.send(new GetTemplateSummaryCommand(input)), + listExports: (input: ListExportsCommandInput): Promise => + client.send(new ListExportsCommand(input)), + listResourceScanRelatedResources: ( + input: ListResourceScanRelatedResourcesCommandInput, + ): Promise => + client.send(new ListResourceScanRelatedResourcesCommand(input)), + listResourceScanResources: ( + input: ListResourceScanResourcesCommandInput, + ): Promise => client.send(new ListResourceScanResourcesCommand(input)), + listResourceScans: (input: ListResourceScansCommandInput): Promise => + client.send(new ListResourceScansCommand(input)), + listStacks: (input: ListStacksCommandInput): Promise => + client.send(new ListStacksCommand(input)), + rollbackStack: (input: RollbackStackCommandInput): Promise => + client.send(new RollbackStackCommand(input)), + startResourceScan: (input: StartResourceScanCommandInput): Promise => + client.send(new StartResourceScanCommand(input)), + updateStack: (input: UpdateStackCommandInput): Promise => + client.send(new UpdateStackCommand(input)), + updateTerminationProtection: ( + input: UpdateTerminationProtectionCommandInput, + ): Promise => + client.send(new UpdateTerminationProtectionCommand(input)), + describeStackEvents: (input: DescribeStackEventsCommandInput): Promise => { + return client.send(new DescribeStackEventsCommand(input)); + }, + listStackResources: async (input: ListStackResourcesCommandInput): Promise => { + const stackResources = Array(); + const paginator = paginateListStackResources({ client }, input); + for await (const page of paginator) { + stackResources.push(...(page?.StackResourceSummaries || [])); + } + return stackResources; + }, + }; + } + + public cloudWatchLogs(): ICloudWatchLogsClient { + const client = new CloudWatchLogsClient(this.config); + return { + describeLogGroups: (input: DescribeLogGroupsCommandInput): Promise => + client.send(new DescribeLogGroupsCommand(input)), + filterLogEvents: (input: FilterLogEventsCommandInput): Promise => + client.send(new FilterLogEventsCommand(input)), + }; + } + + public codeBuild(): ICodeBuildClient { + const client = new CodeBuildClient(this.config); + return { + updateProject: (input: UpdateProjectCommandInput): Promise => + client.send(new UpdateProjectCommand(input)), + }; + } + + public ec2(): IEC2Client { + const client = new EC2Client(this.config); + return { + describeAvailabilityZones: ( + input: DescribeAvailabilityZonesCommandInput, + ): Promise => client.send(new DescribeAvailabilityZonesCommand(input)), + describeImages: (input: DescribeImagesCommandInput): Promise => + client.send(new DescribeImagesCommand(input)), + describeInstances: (input: DescribeInstancesCommandInput): Promise => + client.send(new DescribeInstancesCommand(input)), + describeRouteTables: (input: DescribeRouteTablesCommandInput): Promise => + client.send(new DescribeRouteTablesCommand(input)), + describeSecurityGroups: ( + input: DescribeSecurityGroupsCommandInput, + ): Promise => client.send(new DescribeSecurityGroupsCommand(input)), + describeSubnets: (input: DescribeSubnetsCommandInput): Promise => + client.send(new DescribeSubnetsCommand(input)), + describeVpcEndpointServices: ( + input: DescribeVpcEndpointServicesCommandInput, + ): Promise => + client.send(new DescribeVpcEndpointServicesCommand(input)), + describeVpcs: (input: DescribeVpcsCommandInput): Promise => + client.send(new DescribeVpcsCommand(input)), + describeVpnGateways: (input: DescribeVpnGatewaysCommandInput): Promise => + client.send(new DescribeVpnGatewaysCommand(input)), + }; + } + + public ecr(): IECRClient { + const client = new ECRClient(this.config); + return { + batchDeleteImage: (input: BatchDeleteImageCommandInput): Promise => + client.send(new BatchDeleteImageCommand(input)), + batchGetImage: (input: BatchGetImageCommandInput): Promise => + client.send(new BatchGetImageCommand(input)), + createRepository: (input: CreateRepositoryCommandInput): Promise => + client.send(new CreateRepositoryCommand(input)), + describeImages: (input: ECRDescribeImagesCommandInput): Promise => + client.send(new ECRDescribeImagesCommand(input)), + describeRepositories: (input: DescribeRepositoriesCommandInput): Promise => + client.send(new DescribeRepositoriesCommand(input)), + getAuthorizationToken: (input: GetAuthorizationTokenCommandInput): Promise => + client.send(new GetAuthorizationTokenCommand(input)), + listImages: (input: ListImagesCommandInput): Promise => + client.send(new ListImagesCommand(input)), + putImage: (input: PutImageCommandInput): Promise => + client.send(new PutImageCommand(input)), + putImageScanningConfiguration: ( + input: PutImageScanningConfigurationCommandInput, + ): Promise => + client.send(new PutImageScanningConfigurationCommand(input)), + }; + } + + public ecs(): IECSClient { + const client = new ECSClient(this.config); + return { + listClusters: (input: ListClustersCommandInput): Promise => + client.send(new ListClustersCommand(input)), + registerTaskDefinition: ( + input: RegisterTaskDefinitionCommandInput, + ): Promise => client.send(new RegisterTaskDefinitionCommand(input)), + updateService: (input: UpdateServiceCommandInput): Promise => + client.send(new UpdateServiceCommand(input)), + // Waiters + waitUntilServicesStable: (input: DescribeServicesCommandInput): Promise => { + return waitUntilServicesStable( + { + client, + maxWaitTime: 600, + minDelay: 6, + maxDelay: 6, + }, + input, + ); + }, + }; + } + + public elbv2(): IElasticLoadBalancingV2Client { + const client = new ElasticLoadBalancingV2Client(this.config); + return { + describeListeners: (input: DescribeListenersCommandInput): Promise => + client.send(new DescribeListenersCommand(input)), + describeLoadBalancers: (input: DescribeLoadBalancersCommandInput): Promise => + client.send(new DescribeLoadBalancersCommand(input)), + describeTags: (input: DescribeTagsCommandInput): Promise => + client.send(new DescribeTagsCommand(input)), + // Pagination Functions + paginateDescribeListeners: async (input: DescribeListenersCommandInput): Promise => { + const listeners = Array(); + const paginator = paginateDescribeListeners({ client }, input); + for await (const page of paginator) { + listeners.push(...(page?.Listeners || [])); + } + return listeners; + }, + paginateDescribeLoadBalancers: async (input: DescribeLoadBalancersCommandInput): Promise => { + const loadBalancers = Array(); + const paginator = paginateDescribeLoadBalancers({ client }, input); + for await (const page of paginator) { + loadBalancers.push(...(page?.LoadBalancers || [])); + } + return loadBalancers; + }, + }; + } + + public iam(): IIAMClient { + const client = new IAMClient(this.config); + return { + createPolicy: (input: CreatePolicyCommandInput): Promise => + client.send(new CreatePolicyCommand(input)), + getPolicy: (input: GetPolicyCommandInput): Promise => + client.send(new GetPolicyCommand(input)), + getRole: (input: GetRoleCommandInput): Promise => client.send(new GetRoleCommand(input)), + }; + } + + public kms(): IKMSClient { + const client = new KMSClient(this.config); + return { + describeKey: (input: DescribeKeyCommandInput): Promise => + client.send(new DescribeKeyCommand(input)), + listAliases: (input: ListAliasesCommandInput): Promise => + client.send(new ListAliasesCommand(input)), + }; + } + + public lambda(): ILambdaClient { + const client = new LambdaClient(this.config); + return { + invokeCommand: (input: InvokeCommandInput): Promise => client.send(new InvokeCommand(input)), + publishVersion: (input: PublishVersionCommandInput): Promise => + client.send(new PublishVersionCommand(input)), + updateAlias: (input: UpdateAliasCommandInput): Promise => + client.send(new UpdateAliasCommand(input)), + updateFunctionCode: (input: UpdateFunctionCodeCommandInput): Promise => + client.send(new UpdateFunctionCodeCommand(input)), + updateFunctionConfiguration: ( + input: UpdateFunctionConfigurationCommandInput, + ): Promise => + client.send(new UpdateFunctionConfigurationCommand(input)), + // Waiters + waitUntilFunctionUpdated: ( + delaySeconds: number, + input: UpdateFunctionConfigurationCommandInput, + ): Promise => { + return waitUntilFunctionUpdatedV2( + { + client, + maxDelay: delaySeconds, + minDelay: delaySeconds, + maxWaitTime: delaySeconds * 60, + }, + input, + ); + }, + }; + } + + public route53(): IRoute53Client { + const client = new Route53Client(this.config); + return { + getHostedZone: (input: GetHostedZoneCommandInput): Promise => + client.send(new GetHostedZoneCommand(input)), + listHostedZones: (input: ListHostedZonesCommandInput): Promise => + client.send(new ListHostedZonesCommand(input)), + listHostedZonesByName: (input: ListHostedZonesByNameCommandInput): Promise => + client.send(new ListHostedZonesByNameCommand(input)), + }; + } + + public s3(): IS3Client { + const client = new S3Client(this.config); + return { + deleteObjects: (input: DeleteObjectsCommandInput): Promise => + client.send(new DeleteObjectsCommand({ + ...input, + ChecksumAlgorithm: 'SHA256', + })), + deleteObjectTagging: (input: DeleteObjectTaggingCommandInput): Promise => + client.send(new DeleteObjectTaggingCommand(input)), + getBucketEncryption: (input: GetBucketEncryptionCommandInput): Promise => + client.send(new GetBucketEncryptionCommand(input)), + getBucketLocation: (input: GetBucketLocationCommandInput): Promise => + client.send(new GetBucketLocationCommand(input)), + getObject: (input: GetObjectCommandInput): Promise => + client.send(new GetObjectCommand(input)), + getObjectTagging: (input: GetObjectTaggingCommandInput): Promise => + client.send(new GetObjectTaggingCommand(input)), + listObjectsV2: (input: ListObjectsV2CommandInput): Promise => + client.send(new ListObjectsV2Command(input)), + putObjectTagging: (input: PutObjectTaggingCommandInput): Promise => + client.send(new PutObjectTaggingCommand({ + ...input, + ChecksumAlgorithm: 'SHA256', + })), + upload: (input: PutObjectCommandInput): Promise => { + try { + const upload = new Upload({ + client, + params: { ...input, ChecksumAlgorithm: 'SHA256' }, + }); + + return upload.done(); + } catch (e: any) { + throw new AuthenticationError(`Upload failed: ${formatErrorMessage(e)}`); + } + }, + }; + } + + public secretsManager(): ISecretsManagerClient { + const client = new SecretsManagerClient(this.config); + return { + getSecretValue: (input: GetSecretValueCommandInput): Promise => + client.send(new GetSecretValueCommand(input)), + }; + } + + public ssm(): ISSMClient { + const client = new SSMClient(this.config); + return { + getParameter: (input: GetParameterCommandInput): Promise => + client.send(new GetParameterCommand(input)), + }; + } + + public stepFunctions(): IStepFunctionsClient { + const client = new SFNClient(this.config); + return { + updateStateMachine: (input: UpdateStateMachineCommandInput): Promise => + client.send(new UpdateStateMachineCommand(input)), + }; + } + + /** + * The AWS SDK v3 requires a client config and a command in order to get an endpoint for + * any given service. + */ + public async getUrlSuffix(region: string): Promise { + const cfn = new CloudFormationClient({ region }); + const endpoint = await getEndpointFromInstructions({}, DescribeStackResourcesCommand, { ...cfn.config }); + return endpoint.url.hostname.split(`${region}.`).pop()!; + } + + public async currentAccount(): Promise { + return cachedAsync(this, CURRENT_ACCOUNT_KEY, async () => { + const creds = await this.credProvider(); + return SDK.accountCache.fetch(creds.accessKeyId, async () => { + // if we don't have one, resolve from STS and store in cache. + debug('Looking up default account ID from STS'); + const client = new STSClient({ + ...this.config, + retryStrategy: this.stsRetryStrategy, + }); + const command = new GetCallerIdentityCommand({}); + const result = await client.send(command); + const accountId = result.Account; + const partition = result.Arn!.split(':')[1]; + if (!accountId) { + throw new AuthenticationError("STS didn't return an account ID"); + } + debug('Default account ID:', accountId); + + // Save another STS call later if this one already succeeded + this._credentialsValidated = true; + return { accountId, partition }; + }); + }); + } + + /** + * Make sure the the current credentials are not expired + */ + public async validateCredentials() { + if (this._credentialsValidated) { + return; + } + + const client = new STSClient({ ...this.config, retryStrategy: this.stsRetryStrategy }); + await client.send(new GetCallerIdentityCommand({})); + this._credentialsValidated = true; + } +} + +const CURRENT_ACCOUNT_KEY = Symbol('current_account_key'); diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/user-agent.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/user-agent.ts new file mode 100644 index 0000000000000..98e2f716d57d1 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/user-agent.ts @@ -0,0 +1,17 @@ +import * as path from 'path'; +import { readIfPossible } from './util'; +import { rootDir } from '../../util/directories'; + +/** + * Find the package.json from the main toolkit. + * + * If we can't read it for some reason, try to do something reasonable anyway. + * Fall back to argv[1], or a standard string if that is undefined for some reason. + */ +export function defaultCliUserAgent() { + const root = rootDir(false); + const pkg = JSON.parse((root ? readIfPossible(path.join(root, 'package.json')) : undefined) ?? '{}'); + const name = pkg.name ?? path.basename(process.argv[1] ?? 'cdk-cli'); + const version = pkg.version ?? ''; + return `${name}/${version}`; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/util.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/util.ts new file mode 100644 index 0000000000000..c94fd4790c876 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/aws-auth/util.ts @@ -0,0 +1,19 @@ +import * as fs from 'fs-extra'; +import { debug } from '../logging'; + +/** + * Read a file if it exists, or return undefined + * + * Not async because it is used in the constructor + */ +export function readIfPossible(filename: string): string | undefined { + try { + if (!fs.pathExistsSync(filename)) { + return undefined; + } + return fs.readFileSync(filename, { encoding: 'utf-8' }); + } catch (e: any) { + debug(e); + return undefined; + } +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/bootstrap-environment.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/bootstrap-environment.ts new file mode 100644 index 0000000000000..10b9e5bc22fc8 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/bootstrap-environment.ts @@ -0,0 +1,377 @@ +import { info } from 'console'; +import * as cxapi from '@aws-cdk/cx-api'; +import type { BootstrapEnvironmentOptions, BootstrappingParameters } from './bootstrap-props'; +import { BootstrapStack, bootstrapVersionFromTemplate } from './deploy-bootstrap'; +import { legacyBootstrapTemplate } from './legacy-template'; +import { ToolkitError } from '../../toolkit/error'; +import { resourcePath } from '../../util/directories'; +import { loadStructuredFile, serializeStructure } from '../../util/serialize'; +import type { SDK, SdkProvider } from '../aws-auth'; +import type { SuccessfulDeployStackResult } from '../deploy-stack'; +import { warning } from '../logging'; +import { Mode } from '../plugin/mode'; + +export type BootstrapSource = { source: 'legacy' } | { source: 'default' } | { source: 'custom'; templateFile: string }; + +export class Bootstrapper { + constructor(private readonly source: BootstrapSource = { source: 'default' }) {} + + public bootstrapEnvironment( + environment: cxapi.Environment, + sdkProvider: SdkProvider, + options: BootstrapEnvironmentOptions = {}, + ): Promise { + switch (this.source.source) { + case 'legacy': + return this.legacyBootstrap(environment, sdkProvider, options); + case 'default': + return this.modernBootstrap(environment, sdkProvider, options); + case 'custom': + return this.customBootstrap(environment, sdkProvider, options); + } + } + + public async showTemplate(json: boolean) { + const template = await this.loadTemplate(); + process.stdout.write(`${serializeStructure(template, json)}\n`); + } + + /** + * Deploy legacy bootstrap stack + * + */ + private async legacyBootstrap( + environment: cxapi.Environment, + sdkProvider: SdkProvider, + options: BootstrapEnvironmentOptions = {}, + ): Promise { + const params = options.parameters ?? {}; + + if (params.trustedAccounts?.length) { + throw new ToolkitError('--trust can only be passed for the modern bootstrap experience.'); + } + if (params.cloudFormationExecutionPolicies?.length) { + throw new ToolkitError('--cloudformation-execution-policies can only be passed for the modern bootstrap experience.'); + } + if (params.createCustomerMasterKey !== undefined) { + throw new ToolkitError('--bootstrap-customer-key can only be passed for the modern bootstrap experience.'); + } + if (params.qualifier) { + throw new ToolkitError('--qualifier can only be passed for the modern bootstrap experience.'); + } + + const current = await BootstrapStack.lookup(sdkProvider, environment, options.toolkitStackName); + return current.update( + await this.loadTemplate(params), + {}, + { + ...options, + terminationProtection: options.terminationProtection ?? current.terminationProtection, + }, + ); + } + + /** + * Deploy CI/CD-ready bootstrap stack from template + * + */ + private async modernBootstrap( + environment: cxapi.Environment, + sdkProvider: SdkProvider, + options: BootstrapEnvironmentOptions = {}, + ): Promise { + const params = options.parameters ?? {}; + + const bootstrapTemplate = await this.loadTemplate(); + + const current = await BootstrapStack.lookup(sdkProvider, environment, options.toolkitStackName); + const partition = await current.partition(); + + if (params.createCustomerMasterKey !== undefined && params.kmsKeyId) { + throw new ToolkitError( + "You cannot pass '--bootstrap-kms-key-id' and '--bootstrap-customer-key' together. Specify one or the other", + ); + } + + // If people re-bootstrap, existing parameter values are reused so that people don't accidentally change the configuration + // on their bootstrap stack (this happens automatically in deployStack). However, to do proper validation on the + // combined arguments (such that if --trust has been given, --cloudformation-execution-policies is necessary as well) + // we need to take this parameter reuse into account. + // + // Ideally we'd do this inside the template, but the `Rules` section of CFN + // templates doesn't seem to be able to express the conditions that we need + // (can't use Fn::Join or reference Conditions) so we do it here instead. + const trustedAccounts = params.trustedAccounts ?? splitCfnArray(current.parameters.TrustedAccounts); + info(`Trusted accounts for deployment: ${trustedAccounts.length > 0 ? trustedAccounts.join(', ') : '(none)'}`); + + const trustedAccountsForLookup = + params.trustedAccountsForLookup ?? splitCfnArray(current.parameters.TrustedAccountsForLookup); + info( + `Trusted accounts for lookup: ${trustedAccountsForLookup.length > 0 ? trustedAccountsForLookup.join(', ') : '(none)'}`, + ); + + const cloudFormationExecutionPolicies = + params.cloudFormationExecutionPolicies ?? splitCfnArray(current.parameters.CloudFormationExecutionPolicies); + if (trustedAccounts.length === 0 && cloudFormationExecutionPolicies.length === 0) { + // For self-trust it's okay to default to AdministratorAccess, and it improves the usability of bootstrapping a lot. + // + // We don't actually make the implicitly policy a physical parameter. The template will infer it instead, + // we simply do the UI advertising that behavior here. + // + // If we DID make it an explicit parameter, we wouldn't be able to tell the difference between whether + // we inferred it or whether the user told us, and the sequence: + // + // $ cdk bootstrap + // $ cdk bootstrap --trust 1234 + // + // Would leave AdministratorAccess policies with a trust relationship, without the user explicitly + // approving the trust policy. + const implicitPolicy = `arn:${partition}:iam::aws:policy/AdministratorAccess`; + warning( + `Using default execution policy of '${implicitPolicy}'. Pass '--cloudformation-execution-policies' to customize.`, + ); + } else if (cloudFormationExecutionPolicies.length === 0) { + throw new ToolkitError( + `Please pass \'--cloudformation-execution-policies\' when using \'--trust\' to specify deployment permissions. Try a managed policy of the form \'arn:${partition}:iam::aws:policy/\'.`, + ); + } else { + // Remind people what the current settings are + info(`Execution policies: ${cloudFormationExecutionPolicies.join(', ')}`); + } + + // * If an ARN is given, that ARN. Otherwise: + // * '-' if customerKey = false + // * '' if customerKey = true + // * if customerKey is also not given + // * undefined if we already had a value in place (reusing what we had) + // * '-' if this is the first time we're deploying this stack (or upgrading from old to new bootstrap) + const currentKmsKeyId = current.parameters.FileAssetsBucketKmsKeyId; + const kmsKeyId = + params.kmsKeyId ?? + (params.createCustomerMasterKey === true + ? CREATE_NEW_KEY + : params.createCustomerMasterKey === false || currentKmsKeyId === undefined + ? USE_AWS_MANAGED_KEY + : undefined); + + /* A permissions boundary can be provided via: + * - the flag indicating the example one should be used + * - the name indicating the custom permissions boundary to be used + * Re-bootstrapping will NOT be blocked by either tightening or relaxing the permissions' boundary. + */ + + // InputPermissionsBoundary is an `any` type and if it is not defined it + // appears as an empty string ''. We need to force it to evaluate an empty string + // as undefined + const currentPermissionsBoundary: string | undefined = current.parameters.InputPermissionsBoundary || undefined; + const inputPolicyName = params.examplePermissionsBoundary + ? CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY + : params.customPermissionsBoundary; + let policyName: string | undefined; + if (inputPolicyName) { + // If the example policy is not already in place, it must be created. + const sdk = (await sdkProvider.forEnvironment(environment, Mode.ForWriting)).sdk; + policyName = await this.getPolicyName(environment, sdk, inputPolicyName, partition, params); + } + if (currentPermissionsBoundary !== policyName) { + if (!currentPermissionsBoundary) { + warning(`Adding new permissions boundary ${policyName}`); + } else if (!policyName) { + warning(`Removing existing permissions boundary ${currentPermissionsBoundary}`); + } else { + warning(`Changing permissions boundary from ${currentPermissionsBoundary} to ${policyName}`); + } + } + + return current.update( + bootstrapTemplate, + { + FileAssetsBucketName: params.bucketName, + FileAssetsBucketKmsKeyId: kmsKeyId, + // Empty array becomes empty string + TrustedAccounts: trustedAccounts.join(','), + TrustedAccountsForLookup: trustedAccountsForLookup.join(','), + CloudFormationExecutionPolicies: cloudFormationExecutionPolicies.join(','), + Qualifier: params.qualifier, + PublicAccessBlockConfiguration: + params.publicAccessBlockConfiguration || params.publicAccessBlockConfiguration === undefined + ? 'true' + : 'false', + InputPermissionsBoundary: policyName, + }, + { + ...options, + terminationProtection: options.terminationProtection ?? current.terminationProtection, + }, + ); + } + + private async getPolicyName( + environment: cxapi.Environment, + sdk: SDK, + permissionsBoundary: string, + partition: string, + params: BootstrappingParameters, + ): Promise { + if (permissionsBoundary !== CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY) { + this.validatePolicyName(permissionsBoundary); + return Promise.resolve(permissionsBoundary); + } + // if no Qualifier is supplied, resort to the default one + const arn = await this.getExamplePermissionsBoundary( + params.qualifier ?? 'hnb659fds', + partition, + environment.account, + sdk, + ); + const policyName = arn.split('/').pop(); + if (!policyName) { + throw new ToolkitError('Could not retrieve the example permission boundary!'); + } + return Promise.resolve(policyName); + } + + private async getExamplePermissionsBoundary( + qualifier: string, + partition: string, + account: string, + sdk: SDK, + ): Promise { + const iam = sdk.iam(); + + let policyName = `cdk-${qualifier}-permissions-boundary`; + const arn = `arn:${partition}:iam::${account}:policy/${policyName}`; + + try { + let getPolicyResp = await iam.getPolicy({ PolicyArn: arn }); + if (getPolicyResp.Policy) { + return arn; + } + } catch (e: any) { + // https://docs.aws.amazon.com/IAM/latest/APIReference/API_GetPolicy.html#API_GetPolicy_Errors + if (e.name === 'NoSuchEntity') { + //noop, proceed with creating the policy + } else { + throw e; + } + } + + const policyDoc = { + Version: '2012-10-17', + Statement: [ + { + Action: ['*'], + Resource: '*', + Effect: 'Allow', + Sid: 'ExplicitAllowAll', + }, + { + Condition: { + StringEquals: { + 'iam:PermissionsBoundary': `arn:${partition}:iam::${account}:policy/cdk-${qualifier}-permissions-boundary`, + }, + }, + Action: [ + 'iam:CreateUser', + 'iam:CreateRole', + 'iam:PutRolePermissionsBoundary', + 'iam:PutUserPermissionsBoundary', + ], + Resource: '*', + Effect: 'Allow', + Sid: 'DenyAccessIfRequiredPermBoundaryIsNotBeingApplied', + }, + { + Action: [ + 'iam:CreatePolicyVersion', + 'iam:DeletePolicy', + 'iam:DeletePolicyVersion', + 'iam:SetDefaultPolicyVersion', + ], + Resource: `arn:${partition}:iam::${account}:policy/cdk-${qualifier}-permissions-boundary`, + Effect: 'Deny', + Sid: 'DenyPermBoundaryIAMPolicyAlteration', + }, + { + Action: ['iam:DeleteUserPermissionsBoundary', 'iam:DeleteRolePermissionsBoundary'], + Resource: '*', + Effect: 'Deny', + Sid: 'DenyRemovalOfPermBoundaryFromAnyUserOrRole', + }, + ], + }; + const request = { + PolicyName: policyName, + PolicyDocument: JSON.stringify(policyDoc), + }; + const createPolicyResponse = await iam.createPolicy(request); + if (createPolicyResponse.Policy?.Arn) { + return createPolicyResponse.Policy.Arn; + } else { + throw new ToolkitError(`Could not retrieve the example permission boundary ${arn}!`); + } + } + + private validatePolicyName(permissionsBoundary: string) { + // https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreatePolicy.html + // Added support for policy names with a path + // See https://github.com/aws/aws-cdk/issues/26320 + const regexp: RegExp = /[\w+\/=,.@-]+/; + const matches = regexp.exec(permissionsBoundary); + if (!(matches && matches.length === 1 && matches[0] === permissionsBoundary)) { + throw new ToolkitError(`The permissions boundary name ${permissionsBoundary} does not match the IAM conventions.`); + } + } + + private async customBootstrap( + environment: cxapi.Environment, + sdkProvider: SdkProvider, + options: BootstrapEnvironmentOptions = {}, + ): Promise { + // Look at the template, decide whether it's most likely a legacy or modern bootstrap + // template, and use the right bootstrapper for that. + const version = bootstrapVersionFromTemplate(await this.loadTemplate()); + if (version === 0) { + return this.legacyBootstrap(environment, sdkProvider, options); + } else { + return this.modernBootstrap(environment, sdkProvider, options); + } + } + + private async loadTemplate(params: BootstrappingParameters = {}): Promise { + switch (this.source.source) { + case 'custom': + return loadStructuredFile(this.source.templateFile); + case 'default': + return loadStructuredFile(resourcePath('bootstrap', 'bootstrap-template.yaml')); + case 'legacy': + return legacyBootstrapTemplate(params); + } + } +} + +/** + * Magic parameter value that will cause the bootstrap-template.yml to NOT create a CMK but use the default key + */ +const USE_AWS_MANAGED_KEY = 'AWS_MANAGED_KEY'; + +/** + * Magic parameter value that will cause the bootstrap-template.yml to create a CMK + */ +const CREATE_NEW_KEY = ''; +/** + * Parameter value indicating the use of the default, CDK provided permissions boundary for bootstrap-template.yml + */ +const CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY = 'CDK_BOOTSTRAP_PERMISSIONS_BOUNDARY'; + +/** + * Split an array-like CloudFormation parameter on , + * + * An empty string is the empty array (instead of `['']`). + */ +function splitCfnArray(xs: string | undefined): string[] { + if (xs === '' || xs === undefined) { + return []; + } + return xs.split(','); +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/bootstrap-props.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/bootstrap-props.ts new file mode 100644 index 0000000000000..233c19ac5d1fb --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/bootstrap-props.ts @@ -0,0 +1,141 @@ +import { BootstrapSource } from './bootstrap-environment'; +import { Tag } from '../deployments'; +import { StringWithoutPlaceholders } from '../util/placeholders'; + +export const BUCKET_NAME_OUTPUT = 'BucketName'; +export const REPOSITORY_NAME_OUTPUT = 'ImageRepositoryName'; +export const BUCKET_DOMAIN_NAME_OUTPUT = 'BucketDomainName'; +export const BOOTSTRAP_VERSION_OUTPUT = 'BootstrapVersion'; +export const BOOTSTRAP_VERSION_RESOURCE = 'CdkBootstrapVersion'; +export const BOOTSTRAP_VARIANT_PARAMETER = 'BootstrapVariant'; + +/** + * The assumed vendor of a template in case it is not set + */ +export const DEFAULT_BOOTSTRAP_VARIANT = 'AWS CDK: Default Resources'; + +/** + * Options for the bootstrapEnvironment operation(s) + */ +export interface BootstrapEnvironmentOptions { + readonly toolkitStackName?: string; + readonly roleArn?: StringWithoutPlaceholders; + readonly parameters?: BootstrappingParameters; + readonly force?: boolean; + + /** + * The source of the bootstrap stack + * + * @default - modern v2-style bootstrapping + */ + readonly source?: BootstrapSource; + + /** + * Whether to execute the changeset or only create it and leave it in review. + * @default true + */ + readonly execute?: boolean; + + /** + * Tags for cdktoolkit stack. + * + * @default - None. + */ + readonly tags?: Tag[]; + + /** + * Whether the stacks created by the bootstrap process should be protected from termination. + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-protect-stacks.html + * @default true + */ + readonly terminationProtection?: boolean; + + /** + * Use previous values for unspecified parameters + * + * If not set, all parameters must be specified for every deployment. + * + * @default true + */ + usePreviousParameters?: boolean; +} + +/** + * Parameters for the bootstrapping template + */ +export interface BootstrappingParameters { + /** + * The name to be given to the CDK Bootstrap bucket. + * + * @default - a name is generated by CloudFormation. + */ + readonly bucketName?: string; + + /** + * The ID of an existing KMS key to be used for encrypting items in the bucket. + * + * @default - use the default KMS key or create a custom one + */ + readonly kmsKeyId?: string; + + /** + * Whether or not to create a new customer master key (CMK) + * + * Only applies to modern bootstrapping. Legacy bootstrapping will never create + * a CMK, only use the default S3 key. + * + * @default false + */ + readonly createCustomerMasterKey?: boolean; + + /** + * The list of AWS account IDs that are trusted to deploy into the environment being bootstrapped. + * + * @default - only the bootstrapped account can deploy into this environment + */ + readonly trustedAccounts?: string[]; + + /** + * The list of AWS account IDs that are trusted to look up values in the environment being bootstrapped. + * + * @default - only the bootstrapped account can look up values in this environment + */ + readonly trustedAccountsForLookup?: string[]; + + /** + * The ARNs of the IAM managed policies that should be attached to the role performing CloudFormation deployments. + * In most cases, this will be the AdministratorAccess policy. + * At least one policy is required if `trustedAccounts` were passed. + * + * @default - the role will have no policies attached + */ + readonly cloudFormationExecutionPolicies?: string[]; + + /** + * Identifier to distinguish multiple bootstrapped environments + * + * @default - Default qualifier + */ + readonly qualifier?: string; + + /** + * Whether or not to enable S3 Staging Bucket Public Access Block Configuration + * + * @default true + */ + readonly publicAccessBlockConfiguration?: boolean; + + /** + * Flag for using the default permissions boundary for bootstrapping + * + * @default - No value, optional argument + */ + readonly examplePermissionsBoundary?: boolean; + + /** + * Name for the customer's custom permissions boundary for bootstrapping + * + * @default - No value, optional argument + */ + readonly customPermissionsBoundary?: string; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/deploy-bootstrap.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/deploy-bootstrap.ts new file mode 100644 index 0000000000000..1a089ee2e76a0 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/deploy-bootstrap.ts @@ -0,0 +1,167 @@ +import * as os from 'os'; +import * as path from 'path'; +import { ArtifactType } from '@aws-cdk/cloud-assembly-schema'; +import { CloudAssemblyBuilder, Environment, EnvironmentUtils } from '@aws-cdk/cx-api'; +import * as fs from 'fs-extra'; +import { + BOOTSTRAP_VARIANT_PARAMETER, + BOOTSTRAP_VERSION_OUTPUT, + BOOTSTRAP_VERSION_RESOURCE, + BootstrapEnvironmentOptions, + DEFAULT_BOOTSTRAP_VARIANT, +} from './bootstrap-props'; +import type { SDK, SdkProvider } from '../aws-auth'; +import { assertIsSuccessfulDeployStackResult, deployStack, SuccessfulDeployStackResult } from '../deploy-stack'; +import { NoBootstrapStackEnvironmentResources } from '../environment-resources'; +import * as logging from '../logging'; +import { Mode } from '../plugin/mode'; +import { DEFAULT_TOOLKIT_STACK_NAME, ToolkitInfo } from '../toolkit-info'; + +/** + * A class to hold state around stack bootstrapping + * + * This class exists so we can break bootstrapping into 2 phases: + * + * ```ts + * const current = BootstrapStack.lookup(...); + * // ... + * current.update(newTemplate, ...); + * ``` + * + * And do something in between the two phases (such as look at the + * current bootstrap stack and doing something intelligent). + */ +export class BootstrapStack { + public static async lookup(sdkProvider: SdkProvider, environment: Environment, toolkitStackName?: string) { + toolkitStackName = toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME; + + const resolvedEnvironment = await sdkProvider.resolveEnvironment(environment); + const sdk = (await sdkProvider.forEnvironment(resolvedEnvironment, Mode.ForWriting)).sdk; + + const currentToolkitInfo = await ToolkitInfo.lookup(resolvedEnvironment, sdk, toolkitStackName); + + return new BootstrapStack(sdkProvider, sdk, resolvedEnvironment, toolkitStackName, currentToolkitInfo); + } + + protected constructor( + private readonly sdkProvider: SdkProvider, + private readonly sdk: SDK, + private readonly resolvedEnvironment: Environment, + private readonly toolkitStackName: string, + private readonly currentToolkitInfo: ToolkitInfo, + ) {} + + public get parameters(): Record { + return this.currentToolkitInfo.found ? this.currentToolkitInfo.bootstrapStack.parameters : {}; + } + + public get terminationProtection() { + return this.currentToolkitInfo.found ? this.currentToolkitInfo.bootstrapStack.terminationProtection : undefined; + } + + public async partition(): Promise { + return (await this.sdk.currentAccount()).partition; + } + + /** + * Perform the actual deployment of a bootstrap stack, given a template and some parameters + */ + public async update( + template: any, + parameters: Record, + options: Omit, + ): Promise { + if (this.currentToolkitInfo.found && !options.force) { + // Safety checks + const abortResponse = { + type: 'did-deploy-stack', + noOp: true, + outputs: {}, + stackArn: this.currentToolkitInfo.bootstrapStack.stackId, + } satisfies SuccessfulDeployStackResult; + + // Validate that the bootstrap stack we're trying to replace is from the same variant as the one we're trying to deploy + const currentVariant = this.currentToolkitInfo.variant; + const newVariant = bootstrapVariantFromTemplate(template); + if (currentVariant !== newVariant) { + logging.warning( + `Bootstrap stack already exists, containing '${currentVariant}'. Not overwriting it with a template containing '${newVariant}' (use --force if you intend to overwrite)`, + ); + return abortResponse; + } + + // Validate that we're not downgrading the bootstrap stack + const newVersion = bootstrapVersionFromTemplate(template); + const currentVersion = this.currentToolkitInfo.version; + if (newVersion < currentVersion) { + logging.warning( + `Bootstrap stack already at version ${currentVersion}. Not downgrading it to version ${newVersion} (use --force if you intend to downgrade)`, + ); + if (newVersion === 0) { + // A downgrade with 0 as target version means we probably have a new-style bootstrap in the account, + // and an old-style bootstrap as current target, which means the user probably forgot to put this flag in. + logging.warning("(Did you set the '@aws-cdk/core:newStyleStackSynthesis' feature flag in cdk.json?)"); + } + return abortResponse; + } + } + + const outdir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-bootstrap')); + const builder = new CloudAssemblyBuilder(outdir); + const templateFile = `${this.toolkitStackName}.template.json`; + await fs.writeJson(path.join(builder.outdir, templateFile), template, { + spaces: 2, + }); + + builder.addArtifact(this.toolkitStackName, { + type: ArtifactType.AWS_CLOUDFORMATION_STACK, + environment: EnvironmentUtils.format(this.resolvedEnvironment.account, this.resolvedEnvironment.region), + properties: { + templateFile, + terminationProtection: options.terminationProtection ?? false, + }, + }); + + const assembly = builder.buildAssembly(); + + const ret = await deployStack({ + stack: assembly.getStackByName(this.toolkitStackName), + resolvedEnvironment: this.resolvedEnvironment, + sdk: this.sdk, + sdkProvider: this.sdkProvider, + force: options.force, + roleArn: options.roleArn, + tags: options.tags, + deploymentMethod: { method: 'change-set', execute: options.execute }, + parameters, + usePreviousParameters: options.usePreviousParameters ?? true, + // Obviously we can't need a bootstrap stack to deploy a bootstrap stack + envResources: new NoBootstrapStackEnvironmentResources(this.resolvedEnvironment, this.sdk), + }); + + assertIsSuccessfulDeployStackResult(ret); + + return ret; + } +} + +export function bootstrapVersionFromTemplate(template: any): number { + const versionSources = [ + template.Outputs?.[BOOTSTRAP_VERSION_OUTPUT]?.Value, + template.Resources?.[BOOTSTRAP_VERSION_RESOURCE]?.Properties?.Value, + ]; + + for (const vs of versionSources) { + if (typeof vs === 'number') { + return vs; + } + if (typeof vs === 'string' && !isNaN(parseInt(vs, 10))) { + return parseInt(vs, 10); + } + } + return 0; +} + +export function bootstrapVariantFromTemplate(template: any): string { + return template.Parameters?.[BOOTSTRAP_VARIANT_PARAMETER]?.Default ?? DEFAULT_BOOTSTRAP_VARIANT; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/index.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/index.ts new file mode 100644 index 0000000000000..ad6c0dcb2ec13 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/index.ts @@ -0,0 +1,2 @@ +export * from './bootstrap-environment'; +export * from './bootstrap-props'; diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/legacy-template.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/legacy-template.ts new file mode 100644 index 0000000000000..dd9e89f1f659b --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/bootstrap/legacy-template.ts @@ -0,0 +1,79 @@ +import { BootstrappingParameters, BUCKET_DOMAIN_NAME_OUTPUT, BUCKET_NAME_OUTPUT } from './bootstrap-props'; + +export function legacyBootstrapTemplate(params: BootstrappingParameters): any { + return { + Description: 'The CDK Toolkit Stack. It was created by `cdk bootstrap` and manages resources necessary for managing your Cloud Applications with AWS CDK.', + Conditions: { + UsePublicAccessBlockConfiguration: { + 'Fn::Equals': [ + params.publicAccessBlockConfiguration || params.publicAccessBlockConfiguration === undefined ? 'true' : 'false', + 'true', + ], + }, + }, + Resources: { + StagingBucket: { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: params.bucketName, + AccessControl: 'Private', + BucketEncryption: { + ServerSideEncryptionConfiguration: [{ + ServerSideEncryptionByDefault: { + SSEAlgorithm: 'aws:kms', + KMSMasterKeyID: params.kmsKeyId, + }, + }], + }, + PublicAccessBlockConfiguration: { + 'Fn::If': [ + 'UsePublicAccessBlockConfiguration', + { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + { Ref: 'AWS::NoValue' }, + ], + }, + }, + }, + StagingBucketPolicy: { + Type: 'AWS::S3::BucketPolicy', + Properties: { + Bucket: { Ref: 'StagingBucket' }, + PolicyDocument: { + Id: 'AccessControl', + Version: '2012-10-17', + Statement: [ + { + Sid: 'AllowSSLRequestsOnly', + Action: 's3:*', + Effect: 'Deny', + Resource: [ + { 'Fn::Sub': '${StagingBucket.Arn}' }, + { 'Fn::Sub': '${StagingBucket.Arn}/*' }, + ], + Condition: { + Bool: { 'aws:SecureTransport': 'false' }, + }, + Principal: '*', + }, + ], + }, + }, + }, + }, + Outputs: { + [BUCKET_NAME_OUTPUT]: { + Description: 'The name of the S3 bucket owned by the CDK toolkit stack', + Value: { Ref: 'StagingBucket' }, + }, + [BUCKET_DOMAIN_NAME_OUTPUT]: { + Description: 'The domain name of the S3 bucket owned by the CDK toolkit stack', + Value: { 'Fn::GetAtt': ['StagingBucket', 'RegionalDomainName'] }, + }, + }, + }; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/cloud-assembly.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/cloud-assembly.ts new file mode 100644 index 0000000000000..b83cb3e54751e --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/cloud-assembly.ts @@ -0,0 +1,387 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import * as chalk from 'chalk'; +import { minimatch } from 'minimatch'; +import * as semver from 'semver'; +import { ToolkitError } from '../../toolkit/error'; +import { flatten } from '../../util'; +import { error, info, warning } from '../logging'; + +export enum DefaultSelection { + /** + * Returns an empty selection in case there are no selectors. + */ + None = 'none', + + /** + * If the app includes a single stack, returns it. Otherwise throws an exception. + * This behavior is used by "deploy". + */ + OnlySingle = 'single', + + /** + * Returns all stacks in the main (top level) assembly only. + */ + MainAssembly = 'main', + + /** + * If no selectors are provided, returns all stacks in the app, + * including stacks inside nested assemblies. + */ + AllStacks = 'all', +} + +export interface SelectStacksOptions { + /** + * Extend the selection to upstread/downstream stacks + * @default ExtendedStackSelection.None only select the specified stacks. + */ + extend?: ExtendedStackSelection; + + /** + * The behavior if no selectors are provided. + */ + defaultBehavior: DefaultSelection; + + /** + * Whether to deploy if the app contains no stacks. + * + * @default false + */ + ignoreNoStacks?: boolean; +} + +/** + * When selecting stacks, what other stacks to include because of dependencies + */ +export enum ExtendedStackSelection { + /** + * Don't select any extra stacks + */ + None, + + /** + * Include stacks that this stack depends on + */ + Upstream, + + /** + * Include stacks that depend on this stack + */ + Downstream, +} + +/** + * A specification of which stacks should be selected + */ +export interface StackSelector { + /** + * Whether all stacks at the top level assembly should + * be selected and nothing else + */ + allTopLevel?: boolean; + + /** + * A list of patterns to match the stack hierarchical ids + */ + patterns: string[]; +} + +/** + * A single Cloud Assembly and the operations we do on it to deploy the artifacts inside + */ +export class CloudAssembly { + /** + * The directory this CloudAssembly was read from + */ + public readonly directory: string; + + constructor(public readonly assembly: cxapi.CloudAssembly) { + this.directory = assembly.directory; + } + + public async selectStacks(selector: StackSelector, options: SelectStacksOptions): Promise { + const asm = this.assembly; + const topLevelStacks = asm.stacks; + const stacks = semver.major(asm.version) < 10 ? asm.stacks : asm.stacksRecursively; + const allTopLevel = selector.allTopLevel ?? false; + const patterns = sanitizePatterns(selector.patterns); + + if (stacks.length === 0) { + if (options.ignoreNoStacks) { + return new StackCollection(this, []); + } + throw new ToolkitError('This app contains no stacks'); + } + + if (allTopLevel) { + return this.selectTopLevelStacks(stacks, topLevelStacks, options.extend); + } else if (patterns.length > 0) { + return this.selectMatchingStacks(stacks, patterns, options.extend); + } else { + return this.selectDefaultStacks(stacks, topLevelStacks, options.defaultBehavior); + } + } + + private selectTopLevelStacks( + stacks: cxapi.CloudFormationStackArtifact[], + topLevelStacks: cxapi.CloudFormationStackArtifact[], + extend: ExtendedStackSelection = ExtendedStackSelection.None, + ): StackCollection { + if (topLevelStacks.length > 0) { + return this.extendStacks(topLevelStacks, stacks, extend); + } else { + throw new ToolkitError('No stack found in the main cloud assembly. Use "list" to print manifest'); + } + } + + private selectMatchingStacks( + stacks: cxapi.CloudFormationStackArtifact[], + patterns: string[], + extend: ExtendedStackSelection = ExtendedStackSelection.None, + ): StackCollection { + + const matchingPattern = (pattern: string) => (stack: cxapi.CloudFormationStackArtifact) => minimatch(stack.hierarchicalId, pattern); + const matchedStacks = flatten(patterns.map(pattern => stacks.filter(matchingPattern(pattern)))); + + return this.extendStacks(matchedStacks, stacks, extend); + } + + private selectDefaultStacks( + stacks: cxapi.CloudFormationStackArtifact[], + topLevelStacks: cxapi.CloudFormationStackArtifact[], + defaultSelection: DefaultSelection, + ) { + switch (defaultSelection) { + case DefaultSelection.MainAssembly: + return new StackCollection(this, topLevelStacks); + case DefaultSelection.AllStacks: + return new StackCollection(this, stacks); + case DefaultSelection.None: + return new StackCollection(this, []); + case DefaultSelection.OnlySingle: + if (topLevelStacks.length === 1) { + return new StackCollection(this, topLevelStacks); + } else { + throw new ToolkitError('Since this app includes more than a single stack, specify which stacks to use (wildcards are supported) or specify `--all`\n' + + `Stacks: ${stacks.map(x => x.hierarchicalId).join(' · ')}`); + } + default: + throw new ToolkitError(`invalid default behavior: ${defaultSelection}`); + } + } + + private extendStacks( + matched: cxapi.CloudFormationStackArtifact[], + all: cxapi.CloudFormationStackArtifact[], + extend: ExtendedStackSelection = ExtendedStackSelection.None, + ) { + const allStacks = new Map(); + for (const stack of all) { + allStacks.set(stack.hierarchicalId, stack); + } + + const index = indexByHierarchicalId(matched); + + switch (extend) { + case ExtendedStackSelection.Downstream: + includeDownstreamStacks(index, allStacks); + break; + case ExtendedStackSelection.Upstream: + includeUpstreamStacks(index, allStacks); + break; + } + + // Filter original array because it is in the right order + const selectedList = all.filter(s => index.has(s.hierarchicalId)); + + return new StackCollection(this, selectedList); + } + + /** + * Select a single stack by its ID + */ + public stackById(stackId: string) { + return new StackCollection(this, [this.assembly.getStackArtifact(stackId)]); + } +} + +/** + * A collection of stacks and related artifacts + * + * In practice, not all artifacts in the CloudAssembly are created equal; + * stacks can be selected independently, but other artifacts such as asset + * bundles cannot. + */ +export class StackCollection { + constructor(public readonly assembly: CloudAssembly, public readonly stackArtifacts: cxapi.CloudFormationStackArtifact[]) { + } + + public get stackCount() { + return this.stackArtifacts.length; + } + + public get firstStack() { + if (this.stackCount < 1) { + throw new ToolkitError('StackCollection contains no stack artifacts (trying to access the first one)'); + } + return this.stackArtifacts[0]; + } + + public get stackIds(): string[] { + return this.stackArtifacts.map(s => s.id); + } + + public reversed() { + const arts = [...this.stackArtifacts]; + arts.reverse(); + return new StackCollection(this.assembly, arts); + } + + public filter(predicate: (art: cxapi.CloudFormationStackArtifact) => boolean): StackCollection { + return new StackCollection(this.assembly, this.stackArtifacts.filter(predicate)); + } + + public concat(other: StackCollection): StackCollection { + return new StackCollection(this.assembly, this.stackArtifacts.concat(other.stackArtifacts)); + } + + /** + * Extracts 'aws:cdk:warning|info|error' metadata entries from the stack synthesis + */ + public processMetadataMessages(options: MetadataMessageOptions = {}) { + let warnings = false; + let errors = false; + + for (const stack of this.stackArtifacts) { + for (const message of stack.messages) { + switch (message.level) { + case cxapi.SynthesisMessageLevel.WARNING: + warnings = true; + printMessage(warning, 'Warning', message.id, message.entry); + break; + case cxapi.SynthesisMessageLevel.ERROR: + errors = true; + printMessage(error, 'Error', message.id, message.entry); + break; + case cxapi.SynthesisMessageLevel.INFO: + printMessage(info, 'Info', message.id, message.entry); + break; + } + } + } + + if (errors && !options.ignoreErrors) { + throw new ToolkitError('Found errors'); + } + + if (options.strict && warnings) { + throw new ToolkitError('Found warnings (--strict mode)'); + } + + function printMessage(logFn: (s: string) => void, prefix: string, id: string, entry: cxapi.MetadataEntry) { + logFn(`[${prefix} at ${id}] ${entry.data}`); + + if (options.verbose && entry.trace) { + logFn(` ${entry.trace.join('\n ')}`); + } + } + } +} + +export interface MetadataMessageOptions { + /** + * Whether to be verbose + * + * @default false + */ + verbose?: boolean; + + /** + * Don't stop on error metadata + * + * @default false + */ + ignoreErrors?: boolean; + + /** + * Treat warnings in metadata as errors + * + * @default false + */ + strict?: boolean; +} + +function indexByHierarchicalId(stacks: cxapi.CloudFormationStackArtifact[]): Map { + const result = new Map(); + + for (const stack of stacks) { + result.set(stack.hierarchicalId, stack); + } + + return result; +} + +/** + * Calculate the transitive closure of stack dependents. + * + * Modifies `selectedStacks` in-place. + */ +function includeDownstreamStacks( + selectedStacks: Map, + allStacks: Map) { + const added = new Array(); + + let madeProgress; + do { + madeProgress = false; + + for (const [id, stack] of allStacks) { + // Select this stack if it's not selected yet AND it depends on a stack that's in the selected set + if (!selectedStacks.has(id) && (stack.dependencies || []).some(dep => selectedStacks.has(dep.id))) { + selectedStacks.set(id, stack); + added.push(id); + madeProgress = true; + } + } + } while (madeProgress); + + if (added.length > 0) { + info('Including depending stacks: %s', chalk.bold(added.join(', '))); + } +} + +/** + * Calculate the transitive closure of stack dependencies. + * + * Modifies `selectedStacks` in-place. + */ +function includeUpstreamStacks( + selectedStacks: Map, + allStacks: Map) { + const added = new Array(); + let madeProgress = true; + while (madeProgress) { + madeProgress = false; + + for (const stack of selectedStacks.values()) { + // Select an additional stack if it's not selected yet and a dependency of a selected stack (and exists, obviously) + for (const dependencyId of stack.dependencies.map(x => x.manifest.displayName ?? x.id)) { + if (!selectedStacks.has(dependencyId) && allStacks.has(dependencyId)) { + added.push(dependencyId); + selectedStacks.set(dependencyId, allStacks.get(dependencyId)!); + madeProgress = true; + } + } + } + } + + if (added.length > 0) { + info('Including dependency stacks: %s', chalk.bold(added.join(', '))); + } +} + +function sanitizePatterns(patterns: string[]): string[] { + let sanitized = patterns.filter(s => s != null); // filter null/undefined + sanitized = [...new Set(sanitized)]; // make them unique + return sanitized; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/cloud-executable.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/cloud-executable.ts new file mode 100644 index 0000000000000..1b2639655aa69 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/cloud-executable.ts @@ -0,0 +1,126 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import { CloudAssembly } from './cloud-assembly'; +import * as contextproviders from '../../context-providers'; +import { ToolkitError } from '../../toolkit/error'; +import { SdkProvider } from '../aws-auth'; +import { debug } from '../logging'; +import { Configuration } from '../settings'; + +/** + * @returns output directory + */ +export type Synthesizer = (aws: SdkProvider, config: Configuration) => Promise; + +export interface CloudExecutableProps { + /** + * Application configuration (settings and context) + */ + configuration: Configuration; + + /** + * AWS object (used by synthesizer and contextprovider) + */ + sdkProvider: SdkProvider; + + /** + * Callback invoked to synthesize the actual stacks + */ + synthesizer: Synthesizer; +} + +/** + * Represent the Cloud Executable and the synthesis we can do on it + */ +export class CloudExecutable { + private _cloudAssembly?: CloudAssembly; + + constructor(private readonly props: CloudExecutableProps) { + } + + /** + * Return whether there is an app command from the configuration + */ + public get hasApp() { + return !!this.props.configuration.settings.get(['app']); + } + + /** + * Synthesize a set of stacks. + * + * @param cacheCloudAssembly whether to cache the Cloud Assembly after it has been first synthesized. + * This is 'true' by default, and only set to 'false' for 'cdk watch', + * which needs to re-synthesize the Assembly each time it detects a change to the project files + */ + public async synthesize(cacheCloudAssembly: boolean = true): Promise { + if (!this._cloudAssembly || !cacheCloudAssembly) { + this._cloudAssembly = await this.doSynthesize(); + } + return this._cloudAssembly; + } + + private async doSynthesize(): Promise { + // We may need to run the cloud executable multiple times in order to satisfy all missing context + // (When the executable runs, it will tell us about context it wants to use + // but it missing. We'll then look up the context and run the executable again, and + // again, until it doesn't complain anymore or we've stopped making progress). + let previouslyMissingKeys: Set | undefined; + while (true) { + const assembly = await this.props.synthesizer(this.props.sdkProvider, this.props.configuration); + + if (assembly.manifest.missing && assembly.manifest.missing.length > 0) { + const missingKeys = missingContextKeys(assembly.manifest.missing); + + if (!this.canLookup) { + throw new ToolkitError( + 'Context lookups have been disabled. ' + + 'Make sure all necessary context is already in \'cdk.context.json\' by running \'cdk synth\' on a machine with sufficient AWS credentials and committing the result. ' + + `Missing context keys: '${Array.from(missingKeys).join(', ')}'`); + } + + let tryLookup = true; + if (previouslyMissingKeys && setsEqual(missingKeys, previouslyMissingKeys)) { + debug('Not making progress trying to resolve environmental context. Giving up.'); + tryLookup = false; + } + + previouslyMissingKeys = missingKeys; + + if (tryLookup) { + debug('Some context information is missing. Fetching...'); + + await contextproviders.provideContextValues( + assembly.manifest.missing, + this.props.configuration.context, + this.props.sdkProvider); + + // Cache the new context to disk + await this.props.configuration.saveContext(); + + // Execute again + continue; + } + } + + return new CloudAssembly(assembly); + } + } + + private get canLookup() { + return !!(this.props.configuration.settings.get(['lookups']) ?? true); + } +} + +/** + * Return all keys of missing context items + */ +function missingContextKeys(missing?: cxapi.MissingContext[]): Set { + return new Set((missing || []).map(m => m.key)); +} + +function setsEqual(a: Set, b: Set) { + if (a.size !== b.size) { return false; } + for (const x of a) { + if (!b.has(x)) { return false; } + } + return true; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/environments.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/environments.ts new file mode 100644 index 0000000000000..a1b8c66e6f442 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/environments.ts @@ -0,0 +1,69 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import { minimatch } from 'minimatch'; +import { StackCollection } from './cloud-assembly'; +import { ToolkitError } from '../../toolkit/error'; +import { SdkProvider } from '../aws-auth'; + +export function looksLikeGlob(environment: string) { + return environment.indexOf('*') > -1; +} + +// eslint-disable-next-line max-len +export async function globEnvironmentsFromStacks(stacks: StackCollection, environmentGlobs: string[], sdk: SdkProvider): Promise { + if (environmentGlobs.length === 0) { return []; } + + const availableEnvironments = new Array(); + for (const stack of stacks.stackArtifacts) { + const actual = await sdk.resolveEnvironment(stack.environment); + availableEnvironments.push(actual); + } + + const environments = distinct(availableEnvironments).filter(env => environmentGlobs.find(glob => minimatch(env!.name, glob))); + if (environments.length === 0) { + const globs = JSON.stringify(environmentGlobs); + const envList = availableEnvironments.length > 0 ? availableEnvironments.map(env => env!.name).join(', ') : ''; + throw new ToolkitError(`No environments were found when selecting across ${globs} (available: ${envList})`); + } + + return environments; +} + +/** + * Given a set of "/" strings, construct environments for them + */ +export function environmentsFromDescriptors(envSpecs: string[]): cxapi.Environment[] { + const ret = new Array(); + + for (const spec of envSpecs) { + const parts = spec.replace(/^aws:\/\//, '').split('/'); + if (parts.length !== 2) { + throw new ToolkitError(`Expected environment name in format 'aws:///', got: ${spec}`); + } + + ret.push({ + name: spec, + account: parts[0], + region: parts[1], + }); + } + + return ret; +} + +/** + * De-duplicates a list of environments, such that a given account and region is only represented exactly once + * in the result. + * + * @param envs the possibly full-of-duplicates list of environments. + * + * @return a de-duplicated list of environments. + */ +function distinct(envs: cxapi.Environment[]): cxapi.Environment[] { + const unique: { [id: string]: cxapi.Environment } = {}; + for (const env of envs) { + const id = `${env.account || 'default'}/${env.region || 'default'}`; + if (id in unique) { continue; } + unique[id] = env; + } + return Object.values(unique); +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/exec.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/exec.ts new file mode 100644 index 0000000000000..b4e9d5d98959f --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/cxapp/exec.ts @@ -0,0 +1,310 @@ +import * as childProcess from 'child_process'; +import * as os from 'os'; +import * as path from 'path'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as fs from 'fs-extra'; +import * as semver from 'semver'; +import { ToolkitError } from '../../toolkit/error'; +import { splitBySize } from '../../util/objects'; +import { loadTree, some } from '../../util/tree'; +import { SdkProvider } from '../aws-auth'; +import { debug, warning } from '../logging'; +import { Configuration, PROJECT_CONFIG, USER_DEFAULTS } from '../settings'; +import { RWLock, ILock } from '../util/rwlock'; + +export interface ExecProgramResult { + readonly assembly: cxapi.CloudAssembly; + readonly lock: ILock; +} + +/** Invokes the cloud executable and returns JSON output */ +export async function execProgram(aws: SdkProvider, config: Configuration, cliVersionNumber: string): Promise { + const env = await prepareDefaultEnvironment(aws); + const context = await prepareContext(config, env); + + const build = config.settings.get(['build']); + if (build) { + await exec(build); + } + + const app = config.settings.get(['app']); + if (!app) { + throw new ToolkitError(`--app is required either in command-line, in ${PROJECT_CONFIG} or in ${USER_DEFAULTS}`); + } + + // bypass "synth" if app points to a cloud assembly + if (await fs.pathExists(app) && (await fs.stat(app)).isDirectory()) { + debug('--app points to a cloud assembly, so we bypass synth'); + + // Acquire a read lock on this directory + const lock = await new RWLock(app).acquireRead(); + + return { assembly: createAssembly(app), lock }; + } + + const commandLine = await guessExecutable(appToArray(app)); + + const outdir = config.settings.get(['output']); + if (!outdir) { + throw new ToolkitError('unexpected: --output is required'); + } + if (typeof outdir !== 'string') { + throw new ToolkitError(`--output takes a string, got ${JSON.stringify(outdir)}`); + } + try { + await fs.mkdirp(outdir); + } catch (error: any) { + throw new ToolkitError(`Could not create output directory ${outdir} (${error.message})`); + } + + debug('outdir:', outdir); + env[cxapi.OUTDIR_ENV] = outdir; + + // Acquire a lock on the output directory + const writerLock = await new RWLock(outdir).acquireWrite(); + + try { + // Send version information + env[cxapi.CLI_ASM_VERSION_ENV] = cxschema.Manifest.version(); + env[cxapi.CLI_VERSION_ENV] = cliVersionNumber; + + debug('env:', env); + + const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072; + const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit)); + + // Store the safe part in the environment variable + env[cxapi.CONTEXT_ENV] = JSON.stringify(smallContext); + + // If there was any overflow, write it to a temporary file + let contextOverflowLocation; + if (Object.keys(overflow ?? {}).length > 0) { + const contextDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-context')); + contextOverflowLocation = path.join(contextDir, 'context-overflow.json'); + fs.writeJSONSync(contextOverflowLocation, overflow); + env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextOverflowLocation; + } + + await exec(commandLine.join(' ')); + + const assembly = createAssembly(outdir); + + contextOverflowCleanup(contextOverflowLocation, assembly); + + return { assembly, lock: await writerLock.convertToReaderLock() }; + } catch (e) { + await writerLock.release(); + throw e; + } + + async function exec(commandAndArgs: string) { + return new Promise((ok, fail) => { + // We use a slightly lower-level interface to: + // + // - Pass arguments in an array instead of a string, to get around a + // number of quoting issues introduced by the intermediate shell layer + // (which would be different between Linux and Windows). + // + // - Inherit stderr from controlling terminal. We don't use the captured value + // anyway, and if the subprocess is printing to it for debugging purposes the + // user gets to see it sooner. Plus, capturing doesn't interact nicely with some + // processes like Maven. + const proc = childProcess.spawn(commandAndArgs, { + stdio: ['ignore', 'inherit', 'inherit'], + detached: false, + shell: true, + env: { + ...process.env, + ...env, + }, + }); + + proc.on('error', fail); + + proc.on('exit', code => { + if (code === 0) { + return ok(); + } else { + debug('failed command:', commandAndArgs); + return fail(new ToolkitError(`Subprocess exited with error ${code}`)); + } + }); + }); + } +} + +/** + * Creates an assembly with error handling + */ +export function createAssembly(appDir: string) { + try { + return new cxapi.CloudAssembly(appDir, { + // We sort as we deploy + topoSort: false, + }); + } catch (error: any) { + if (error.message.includes(cxschema.VERSION_MISMATCH)) { + // this means the CLI version is too old. + // we instruct the user to upgrade. + throw new ToolkitError(`This CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version.\n(${error.message})`); + } + throw error; + } +} + +/** + * If we don't have region/account defined in context, we fall back to the default SDK behavior + * where region is retrieved from ~/.aws/config and account is based on default credentials provider + * chain and then STS is queried. + * + * This is done opportunistically: for example, if we can't access STS for some reason or the region + * is not configured, the context value will be 'null' and there could failures down the line. In + * some cases, synthesis does not require region/account information at all, so that might be perfectly + * fine in certain scenarios. + * + * @param context The context key/value bash. + */ +export async function prepareDefaultEnvironment(aws: SdkProvider): Promise<{ [key: string]: string }> { + const env: { [key: string]: string } = { }; + + env[cxapi.DEFAULT_REGION_ENV] = aws.defaultRegion; + debug(`Setting "${cxapi.DEFAULT_REGION_ENV}" environment variable to`, env[cxapi.DEFAULT_REGION_ENV]); + + const accountId = (await aws.defaultAccount())?.accountId; + if (accountId) { + env[cxapi.DEFAULT_ACCOUNT_ENV] = accountId; + debug(`Setting "${cxapi.DEFAULT_ACCOUNT_ENV}" environment variable to`, env[cxapi.DEFAULT_ACCOUNT_ENV]); + } + + return env; +} + +/** + * Settings related to synthesis are read from context. + * The merging of various configuration sources like cli args or cdk.json has already happened. + * We now need to set the final values to the context. + */ +export async function prepareContext(config: Configuration, env: { [key: string]: string | undefined}) { + const context = config.context.all; + + const debugMode: boolean = config.settings.get(['debug']) ?? true; + if (debugMode) { + env.CDK_DEBUG = 'true'; + } + + const pathMetadata: boolean = config.settings.get(['pathMetadata']) ?? true; + if (pathMetadata) { + context[cxapi.PATH_METADATA_ENABLE_CONTEXT] = true; + } + + const assetMetadata: boolean = config.settings.get(['assetMetadata']) ?? true; + if (assetMetadata) { + context[cxapi.ASSET_RESOURCE_METADATA_ENABLED_CONTEXT] = true; + } + + const versionReporting: boolean = config.settings.get(['versionReporting']) ?? true; + if (versionReporting) { context[cxapi.ANALYTICS_REPORTING_ENABLED_CONTEXT] = true; } + // We need to keep on doing this for framework version from before this flag was deprecated. + if (!versionReporting) { context['aws:cdk:disable-version-reporting'] = true; } + + const stagingEnabled = config.settings.get(['staging']) ?? true; + if (!stagingEnabled) { + context[cxapi.DISABLE_ASSET_STAGING_CONTEXT] = true; + } + + const bundlingStacks = config.settings.get(['bundlingStacks']) ?? ['**']; + context[cxapi.BUNDLING_STACKS] = bundlingStacks; + + debug('context:', context); + + return context; +} + +/** + * Make sure the 'app' is an array + * + * If it's a string, split on spaces as a trivial way of tokenizing the command line. + */ +function appToArray(app: any) { + return typeof app === 'string' ? app.split(' ') : app; +} + +type CommandGenerator = (file: string) => string[]; + +/** + * Execute the given file with the same 'node' process as is running the current process + */ +function executeNode(scriptFile: string): string[] { + return [process.execPath, scriptFile]; +} + +/** + * Mapping of extensions to command-line generators + */ +const EXTENSION_MAP = new Map([ + ['.js', executeNode], +]); + +/** + * Guess the executable from the command-line argument + * + * Only do this if the file is NOT marked as executable. If it is, + * we'll defer to the shebang inside the file itself. + * + * If we're on Windows, we ALWAYS take the handler, since it's hard to + * verify if registry associations have or have not been set up for this + * file type, so we'll assume the worst and take control. + */ +async function guessExecutable(commandLine: string[]) { + if (commandLine.length === 1) { + let fstat; + + try { + fstat = await fs.stat(commandLine[0]); + } catch { + debug(`Not a file: '${commandLine[0]}'. Using '${commandLine}' as command-line`); + return commandLine; + } + + // eslint-disable-next-line no-bitwise + const isExecutable = (fstat.mode & fs.constants.X_OK) !== 0; + const isWindows = process.platform === 'win32'; + + const handler = EXTENSION_MAP.get(path.extname(commandLine[0])); + if (handler && (!isExecutable || isWindows)) { + return handler(commandLine[0]); + } + } + return commandLine; +} + +function contextOverflowCleanup(location: string | undefined, assembly: cxapi.CloudAssembly) { + if (location) { + fs.removeSync(path.dirname(location)); + + const tree = loadTree(assembly); + const frameworkDoesNotSupportContextOverflow = some(tree, node => { + const fqn = node.constructInfo?.fqn; + const version = node.constructInfo?.version; + return (fqn === 'aws-cdk-lib.App' && version != null && semver.lte(version, '2.38.0')) + || fqn === '@aws-cdk/core.App'; // v1 + }); + + // We're dealing with an old version of the framework here. It is unaware of the temporary + // file, which means that it will ignore the context overflow. + if (frameworkDoesNotSupportContextOverflow) { + warning('Part of the context could not be sent to the application. Please update the AWS CDK library to the latest version.'); + } + } +} + +function spaceAvailableForContext(env: { [key: string]: string }, limit: number) { + const size = (value: string) => value != null ? Buffer.byteLength(value) : 0; + + const usedSpace = Object.entries(env) + .map(([k, v]) => k === cxapi.CONTEXT_ENV ? size(k) : size(k) + size(v)) + .reduce((a, b) => a + b, 0); + + return Math.max(0, limit - usedSpace); +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/deploy-stack.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/deploy-stack.ts new file mode 100644 index 0000000000000..32772916d04fe --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/deploy-stack.ts @@ -0,0 +1,876 @@ +import * as cxapi from '@aws-cdk/cx-api'; +import type { + CreateChangeSetCommandInput, + CreateStackCommandInput, + DescribeChangeSetCommandOutput, + ExecuteChangeSetCommandInput, + UpdateStackCommandInput, + Tag, +} from '@aws-sdk/client-cloudformation'; +import * as chalk from 'chalk'; +import * as uuid from 'uuid'; +import { addMetadataAssetsToManifest } from './assets'; +import type { SDK, SdkProvider, ICloudFormationClient } from './aws-auth'; +import type { EnvironmentResources } from './environment-resources'; +import { CfnEvaluationException } from './evaluate-cloudformation-template'; +import { HotswapMode, HotswapPropertyOverrides, ICON } from './hotswap/common'; +import { tryHotswapDeployment } from './hotswap-deployments'; +import { debug, info, warning } from './logging'; +import { + changeSetHasNoChanges, + CloudFormationStack, + TemplateParameters, + waitForChangeSet, + waitForStackDeploy, + waitForStackDelete, + ParameterValues, + ParameterChanges, + ResourcesToImport, +} from './util/cloudformation'; +import { AssetManifestBuilder } from '../util/asset-manifest-builder'; +import { determineAllowCrossAccountAssetPublishing } from './util/checks'; +import { StackActivityMonitor, type StackActivityProgress } from './util/cloudformation/stack-activity-monitor'; +import { type TemplateBodyParameter, makeBodyParameter } from './util/template-body-parameter'; +import { publishAssets } from '../util/asset-publishing'; +import { StringWithoutPlaceholders } from './util/placeholders'; +import { formatErrorMessage } from '../util/error'; + +export type DeployStackResult = + | SuccessfulDeployStackResult + | NeedRollbackFirstDeployStackResult + | ReplacementRequiresRollbackStackResult + ; + +/** Successfully deployed a stack */ +export interface SuccessfulDeployStackResult { + readonly type: 'did-deploy-stack'; + readonly noOp: boolean; + readonly outputs: { [name: string]: string }; + readonly stackArn: string; +} + +/** The stack is currently in a failpaused state, and needs to be rolled back before the deployment */ +export interface NeedRollbackFirstDeployStackResult { + readonly type: 'failpaused-need-rollback-first'; + readonly reason: 'not-norollback' | 'replacement'; + readonly status: string; +} + +/** The upcoming change has a replacement, which requires deploying with --rollback */ +export interface ReplacementRequiresRollbackStackResult { + readonly type: 'replacement-requires-rollback'; +} + +export function assertIsSuccessfulDeployStackResult(x: DeployStackResult): asserts x is SuccessfulDeployStackResult { + if (x.type !== 'did-deploy-stack') { + throw new Error(`Unexpected deployStack result. This should not happen: ${JSON.stringify(x)}. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose.`); + } +} + +export interface DeployStackOptions { + /** + * The stack to be deployed + */ + readonly stack: cxapi.CloudFormationStackArtifact; + + /** + * The environment to deploy this stack in + * + * The environment on the stack artifact may be unresolved, this one + * must be resolved. + */ + readonly resolvedEnvironment: cxapi.Environment; + + /** + * The SDK to use for deploying the stack + * + * Should have been initialized with the correct role with which + * stack operations should be performed. + */ + readonly sdk: SDK; + + /** + * SDK provider (seeded with default credentials) + * + * Will be used to: + * + * - Publish assets, either legacy assets or large CFN templates + * that aren't themselves assets from a manifest. (Needs an SDK + * Provider because the file publishing role is declared as part + * of the asset). + * - Hotswap + */ + readonly sdkProvider: SdkProvider; + + /** + * Information about the bootstrap stack found in the target environment + */ + readonly envResources: EnvironmentResources; + + /** + * Role to pass to CloudFormation to execute the change set + * + * To obtain a `StringWithoutPlaceholders`, run a regular + * string though `TargetEnvironment.replacePlaceholders`. + * + * @default - No execution role; CloudFormation either uses the role currently associated with + * the stack, or otherwise uses current AWS credentials. + */ + readonly roleArn?: StringWithoutPlaceholders; + + /** + * Notification ARNs to pass to CloudFormation to notify when the change set has completed + * + * @default - No notifications + */ + readonly notificationArns?: string[]; + + /** + * Name to deploy the stack under + * + * @default - Name from assembly + */ + readonly deployName?: string; + + /** + * Quiet or verbose deployment + * + * @default false + */ + readonly quiet?: boolean; + + /** + * List of asset IDs which shouldn't be built + * + * @default - Build all assets + */ + readonly reuseAssets?: string[]; + + /** + * Tags to pass to CloudFormation to add to stack + * + * @default - No tags + */ + readonly tags?: Tag[]; + + /** + * What deployment method to use + * + * @default - Change set with defaults + */ + readonly deploymentMethod?: DeploymentMethod; + + /** + * The collection of extra parameters + * (in addition to those used for assets) + * to pass to the deployed template. + * Note that parameters with `undefined` or empty values will be ignored, + * and not passed to the template. + * + * @default - no additional parameters will be passed to the template + */ + readonly parameters?: { [name: string]: string | undefined }; + + /** + * Use previous values for unspecified parameters + * + * If not set, all parameters must be specified for every deployment. + * + * @default false + */ + readonly usePreviousParameters?: boolean; + + /** + * Display mode for stack deployment progress. + * + * @default StackActivityProgress.Bar stack events will be displayed for + * the resource currently being deployed. + */ + readonly progress?: StackActivityProgress; + + /** + * Deploy even if the deployed template is identical to the one we are about to deploy. + * @default false + */ + readonly force?: boolean; + + /** + * Whether we are on a CI system + * + * @default false + */ + readonly ci?: boolean; + + /** + * Rollback failed deployments + * + * @default true + */ + readonly rollback?: boolean; + + /* + * Whether to perform a 'hotswap' deployment. + * A 'hotswap' deployment will attempt to short-circuit CloudFormation + * and update the affected resources like Lambda functions directly. + * + * @default - `HotswapMode.FULL_DEPLOYMENT` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments + */ + readonly hotswap?: HotswapMode; + + /** + * Extra properties that configure hotswap behavior + */ + readonly hotswapPropertyOverrides?: HotswapPropertyOverrides; + + /** + * The extra string to append to the User-Agent header when performing AWS SDK calls. + * + * @default - nothing extra is appended to the User-Agent header + */ + readonly extraUserAgent?: string; + + /** + * If set, change set of type IMPORT will be created, and resourcesToImport + * passed to it. + */ + readonly resourcesToImport?: ResourcesToImport; + + /** + * If present, use this given template instead of the stored one + * + * @default - Use the stored template + */ + readonly overrideTemplate?: any; + + /** + * Whether to build/publish assets in parallel + * + * @default true To remain backward compatible. + */ + readonly assetParallelism?: boolean; +} + +export type DeploymentMethod = DirectDeploymentMethod | ChangeSetDeploymentMethod; + +export interface DirectDeploymentMethod { + readonly method: 'direct'; +} + +export interface ChangeSetDeploymentMethod { + readonly method: 'change-set'; + + /** + * Whether to execute the changeset or leave it in review. + * + * @default true + */ + readonly execute?: boolean; + + /** + * Optional name to use for the CloudFormation change set. + * If not provided, a name will be generated automatically. + */ + readonly changeSetName?: string; + + /** + * Indicates if the change set imports resources that already exist. + * + * @default false + */ + readonly importExistingResources?: boolean; +} + +export async function deployStack(options: DeployStackOptions): Promise { + const stackArtifact = options.stack; + + const stackEnv = options.resolvedEnvironment; + + options.sdk.appendCustomUserAgent(options.extraUserAgent); + const cfn = options.sdk.cloudFormation(); + const deployName = options.deployName || stackArtifact.stackName; + let cloudFormationStack = await CloudFormationStack.lookup(cfn, deployName); + + if (cloudFormationStack.stackStatus.isCreationFailure) { + debug( + `Found existing stack ${deployName} that had previously failed creation. Deleting it before attempting to re-create it.`, + ); + await cfn.deleteStack({ StackName: deployName }); + const deletedStack = await waitForStackDelete(cfn, deployName); + if (deletedStack && deletedStack.stackStatus.name !== 'DELETE_COMPLETE') { + throw new Error( + `Failed deleting stack ${deployName} that had previously failed creation (current state: ${deletedStack.stackStatus})`, + ); + } + // Update variable to mark that the stack does not exist anymore, but avoid + // doing an actual lookup in CloudFormation (which would be silly to do if + // we just deleted it). + cloudFormationStack = CloudFormationStack.doesNotExist(cfn, deployName); + } + + // Detect "legacy" assets (which remain in the metadata) and publish them via + // an ad-hoc asset manifest, while passing their locations via template + // parameters. + const legacyAssets = new AssetManifestBuilder(); + const assetParams = await addMetadataAssetsToManifest( + stackArtifact, + legacyAssets, + options.envResources, + options.reuseAssets, + ); + + const finalParameterValues = { ...options.parameters, ...assetParams }; + + const templateParams = TemplateParameters.fromTemplate(stackArtifact.template); + const stackParams = options.usePreviousParameters + ? templateParams.updateExisting(finalParameterValues, cloudFormationStack.parameters) + : templateParams.supplyAll(finalParameterValues); + + const hotswapMode = options.hotswap ?? HotswapMode.FULL_DEPLOYMENT; + const hotswapPropertyOverrides = options.hotswapPropertyOverrides ?? new HotswapPropertyOverrides(); + + if (await canSkipDeploy(options, cloudFormationStack, stackParams.hasChanges(cloudFormationStack.parameters))) { + debug(`${deployName}: skipping deployment (use --force to override)`); + // if we can skip deployment and we are performing a hotswap, let the user know + // that no hotswap deployment happened + if (hotswapMode !== HotswapMode.FULL_DEPLOYMENT) { + info( + `\n ${ICON} %s\n`, + chalk.bold('hotswap deployment skipped - no changes were detected (use --force to override)'), + ); + } + return { + type: 'did-deploy-stack', + noOp: true, + outputs: cloudFormationStack.outputs, + stackArn: cloudFormationStack.stackId, + }; + } else { + debug(`${deployName}: deploying...`); + } + + const bodyParameter = await makeBodyParameter( + stackArtifact, + options.resolvedEnvironment, + legacyAssets, + options.envResources, + options.overrideTemplate, + ); + let bootstrapStackName: string | undefined; + try { + bootstrapStackName = (await options.envResources.lookupToolkit()).stackName; + } catch (e) { + debug(`Could not determine the bootstrap stack name: ${e}`); + } + await publishAssets(legacyAssets.toManifest(stackArtifact.assembly.directory), options.sdkProvider, stackEnv, { + parallel: options.assetParallelism, + allowCrossAccount: await determineAllowCrossAccountAssetPublishing(options.sdk, bootstrapStackName), + }); + + if (hotswapMode !== HotswapMode.FULL_DEPLOYMENT) { + // attempt to short-circuit the deployment if possible + try { + const hotswapDeploymentResult = await tryHotswapDeployment( + options.sdkProvider, + stackParams.values, + cloudFormationStack, + stackArtifact, + hotswapMode, hotswapPropertyOverrides, + ); + if (hotswapDeploymentResult) { + return hotswapDeploymentResult; + } + info( + 'Could not perform a hotswap deployment, as the stack %s contains non-Asset changes', + stackArtifact.displayName, + ); + } catch (e) { + if (!(e instanceof CfnEvaluationException)) { + throw e; + } + info( + 'Could not perform a hotswap deployment, because the CloudFormation template could not be resolved: %s', + formatErrorMessage(e), + ); + } + + if (hotswapMode === HotswapMode.FALL_BACK) { + info('Falling back to doing a full deployment'); + options.sdk.appendCustomUserAgent('cdk-hotswap/fallback'); + } else { + return { + type: 'did-deploy-stack', + noOp: true, + stackArn: cloudFormationStack.stackId, + outputs: cloudFormationStack.outputs, + }; + } + } + + // could not short-circuit the deployment, perform a full CFN deploy instead + const fullDeployment = new FullCloudFormationDeployment( + options, + cloudFormationStack, + stackArtifact, + stackParams, + bodyParameter, + ); + return fullDeployment.performDeployment(); +} + +type CommonPrepareOptions = keyof CreateStackCommandInput & +keyof UpdateStackCommandInput & +keyof CreateChangeSetCommandInput; +type CommonExecuteOptions = keyof CreateStackCommandInput & +keyof UpdateStackCommandInput & +keyof ExecuteChangeSetCommandInput; + +/** + * This class shares state and functionality between the different full deployment modes + */ +class FullCloudFormationDeployment { + private readonly cfn: ICloudFormationClient; + private readonly stackName: string; + private readonly update: boolean; + private readonly verb: string; + private readonly uuid: string; + + constructor( + private readonly options: DeployStackOptions, + private readonly cloudFormationStack: CloudFormationStack, + private readonly stackArtifact: cxapi.CloudFormationStackArtifact, + private readonly stackParams: ParameterValues, + private readonly bodyParameter: TemplateBodyParameter, + ) { + this.cfn = options.sdk.cloudFormation(); + this.stackName = options.deployName ?? stackArtifact.stackName; + + this.update = cloudFormationStack.exists && cloudFormationStack.stackStatus.name !== 'REVIEW_IN_PROGRESS'; + this.verb = this.update ? 'update' : 'create'; + this.uuid = uuid.v4(); + } + + public async performDeployment(): Promise { + const deploymentMethod = this.options.deploymentMethod ?? { + method: 'change-set', + }; + + if (deploymentMethod.method === 'direct' && this.options.resourcesToImport) { + throw new Error('Importing resources requires a changeset deployment'); + } + + switch (deploymentMethod.method) { + case 'change-set': + return this.changeSetDeployment(deploymentMethod); + + case 'direct': + return this.directDeployment(); + } + } + + private async changeSetDeployment(deploymentMethod: ChangeSetDeploymentMethod): Promise { + const changeSetName = deploymentMethod.changeSetName ?? 'cdk-deploy-change-set'; + const execute = deploymentMethod.execute ?? true; + const importExistingResources = deploymentMethod.importExistingResources ?? false; + const changeSetDescription = await this.createChangeSet(changeSetName, execute, importExistingResources); + await this.updateTerminationProtection(); + + if (changeSetHasNoChanges(changeSetDescription)) { + debug('No changes are to be performed on %s.', this.stackName); + if (execute) { + debug('Deleting empty change set %s', changeSetDescription.ChangeSetId); + await this.cfn.deleteChangeSet({ + StackName: this.stackName, + ChangeSetName: changeSetName, + }); + } + + if (this.options.force) { + warning( + [ + 'You used the --force flag, but CloudFormation reported that the deployment would not make any changes.', + 'According to CloudFormation, all resources are already up-to-date with the state in your CDK app.', + '', + 'You cannot use the --force flag to get rid of changes you made in the console. Try using', + 'CloudFormation drift detection instead: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-stack-drift.html', + ].join('\n'), + ); + } + + return { + type: 'did-deploy-stack', + noOp: true, + outputs: this.cloudFormationStack.outputs, + stackArn: changeSetDescription.StackId!, + }; + } + + if (!execute) { + info( + 'Changeset %s created and waiting in review for manual execution (--no-execute)', + changeSetDescription.ChangeSetId, + ); + return { + type: 'did-deploy-stack', + noOp: false, + outputs: this.cloudFormationStack.outputs, + stackArn: changeSetDescription.StackId!, + }; + } + + // If there are replacements in the changeset, check the rollback flag and stack status + const replacement = hasReplacement(changeSetDescription); + const isPausedFailState = this.cloudFormationStack.stackStatus.isRollbackable; + const rollback = this.options.rollback ?? true; + if (isPausedFailState && replacement) { + return { type: 'failpaused-need-rollback-first', reason: 'replacement', status: this.cloudFormationStack.stackStatus.name }; + } + if (isPausedFailState && rollback) { + return { type: 'failpaused-need-rollback-first', reason: 'not-norollback', status: this.cloudFormationStack.stackStatus.name }; + } + if (!rollback && replacement) { + return { type: 'replacement-requires-rollback' }; + } + + return this.executeChangeSet(changeSetDescription); + } + + private async createChangeSet(changeSetName: string, willExecute: boolean, importExistingResources: boolean) { + await this.cleanupOldChangeset(changeSetName); + + debug(`Attempting to create ChangeSet with name ${changeSetName} to ${this.verb} stack ${this.stackName}`); + info('%s: creating CloudFormation changeset...', chalk.bold(this.stackName)); + const changeSet = await this.cfn.createChangeSet({ + StackName: this.stackName, + ChangeSetName: changeSetName, + ChangeSetType: this.options.resourcesToImport ? 'IMPORT' : this.update ? 'UPDATE' : 'CREATE', + ResourcesToImport: this.options.resourcesToImport, + Description: `CDK Changeset for execution ${this.uuid}`, + ClientToken: `create${this.uuid}`, + ImportExistingResources: importExistingResources, + ...this.commonPrepareOptions(), + }); + + debug('Initiated creation of changeset: %s; waiting for it to finish creating...', changeSet.Id); + // Fetching all pages if we'll execute, so we can have the correct change count when monitoring. + return waitForChangeSet(this.cfn, this.stackName, changeSetName, { + fetchAll: willExecute, + }); + } + + private async executeChangeSet(changeSet: DescribeChangeSetCommandOutput): Promise { + debug('Initiating execution of changeset %s on stack %s', changeSet.ChangeSetId, this.stackName); + + await this.cfn.executeChangeSet({ + StackName: this.stackName, + ChangeSetName: changeSet.ChangeSetName!, + ClientRequestToken: `exec${this.uuid}`, + ...this.commonExecuteOptions(), + }); + + debug( + 'Execution of changeset %s on stack %s has started; waiting for the update to complete...', + changeSet.ChangeSetId, + this.stackName, + ); + + // +1 for the extra event emitted from updates. + const changeSetLength: number = (changeSet.Changes ?? []).length + (this.update ? 1 : 0); + return this.monitorDeployment(changeSet.CreationTime!, changeSetLength); + } + + private async cleanupOldChangeset(changeSetName: string) { + if (this.cloudFormationStack.exists) { + // Delete any existing change sets generated by CDK since change set names must be unique. + // The delete request is successful as long as the stack exists (even if the change set does not exist). + debug(`Removing existing change set with name ${changeSetName} if it exists`); + await this.cfn.deleteChangeSet({ + StackName: this.stackName, + ChangeSetName: changeSetName, + }); + } + } + + private async updateTerminationProtection() { + // Update termination protection only if it has changed. + const terminationProtection = this.stackArtifact.terminationProtection ?? false; + if (!!this.cloudFormationStack.terminationProtection !== terminationProtection) { + debug( + 'Updating termination protection from %s to %s for stack %s', + this.cloudFormationStack.terminationProtection, + terminationProtection, + this.stackName, + ); + await this.cfn.updateTerminationProtection({ + StackName: this.stackName, + EnableTerminationProtection: terminationProtection, + }); + debug('Termination protection updated to %s for stack %s', terminationProtection, this.stackName); + } + } + + private async directDeployment(): Promise { + info('%s: %s stack...', chalk.bold(this.stackName), this.update ? 'updating' : 'creating'); + + const startTime = new Date(); + + if (this.update) { + await this.updateTerminationProtection(); + + try { + await this.cfn.updateStack({ + StackName: this.stackName, + ClientRequestToken: `update${this.uuid}`, + ...this.commonPrepareOptions(), + ...this.commonExecuteOptions(), + }); + } catch (err: any) { + if (err.message === 'No updates are to be performed.') { + debug('No updates are to be performed for stack %s', this.stackName); + return { + type: 'did-deploy-stack', + noOp: true, + outputs: this.cloudFormationStack.outputs, + stackArn: this.cloudFormationStack.stackId, + }; + } + throw err; + } + + return this.monitorDeployment(startTime, undefined); + } else { + // Take advantage of the fact that we can set termination protection during create + const terminationProtection = this.stackArtifact.terminationProtection ?? false; + + await this.cfn.createStack({ + StackName: this.stackName, + ClientRequestToken: `create${this.uuid}`, + ...(terminationProtection ? { EnableTerminationProtection: true } : undefined), + ...this.commonPrepareOptions(), + ...this.commonExecuteOptions(), + }); + + return this.monitorDeployment(startTime, undefined); + } + } + + private async monitorDeployment(startTime: Date, expectedChanges: number | undefined): Promise { + const monitor = this.options.quiet + ? undefined + : StackActivityMonitor.withDefaultPrinter(this.cfn, this.stackName, this.stackArtifact, { + resourcesTotal: expectedChanges, + progress: this.options.progress, + changeSetCreationTime: startTime, + ci: this.options.ci, + }).start(); + + let finalState = this.cloudFormationStack; + try { + const successStack = await waitForStackDeploy(this.cfn, this.stackName); + + // This shouldn't really happen, but catch it anyway. You never know. + if (!successStack) { + throw new Error('Stack deploy failed (the stack disappeared while we were deploying it)'); + } + finalState = successStack; + } catch (e: any) { + throw new Error(suffixWithErrors(formatErrorMessage(e), monitor?.errors)); + } finally { + await monitor?.stop(); + } + debug('Stack %s has completed updating', this.stackName); + return { + type: 'did-deploy-stack', + noOp: false, + outputs: finalState.outputs, + stackArn: finalState.stackId, + }; + } + + /** + * Return the options that are shared between CreateStack, UpdateStack and CreateChangeSet + */ + private commonPrepareOptions(): Partial> { + return { + Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], + NotificationARNs: this.options.notificationArns, + Parameters: this.stackParams.apiParameters, + RoleARN: this.options.roleArn, + TemplateBody: this.bodyParameter.TemplateBody, + TemplateURL: this.bodyParameter.TemplateURL, + Tags: this.options.tags, + }; + } + + /** + * Return the options that are shared between UpdateStack and CreateChangeSet + * + * Be careful not to add in keys for options that aren't used, as the features may not have been + * deployed everywhere yet. + */ + private commonExecuteOptions(): Partial> { + const shouldDisableRollback = this.options.rollback === false; + + return { + StackName: this.stackName, + ...(shouldDisableRollback ? { DisableRollback: true } : undefined), + }; + } +} + +export interface DestroyStackOptions { + /** + * The stack to be destroyed + */ + stack: cxapi.CloudFormationStackArtifact; + + sdk: SDK; + roleArn?: string; + deployName?: string; + quiet?: boolean; + ci?: boolean; +} + +export async function destroyStack(options: DestroyStackOptions) { + const deployName = options.deployName || options.stack.stackName; + const cfn = options.sdk.cloudFormation(); + + const currentStack = await CloudFormationStack.lookup(cfn, deployName); + if (!currentStack.exists) { + return; + } + const monitor = options.quiet + ? undefined + : StackActivityMonitor.withDefaultPrinter(cfn, deployName, options.stack, { + ci: options.ci, + }).start(); + + try { + await cfn.deleteStack({ StackName: deployName, RoleARN: options.roleArn }); + const destroyedStack = await waitForStackDelete(cfn, deployName); + if (destroyedStack && destroyedStack.stackStatus.name !== 'DELETE_COMPLETE') { + throw new Error(`Failed to destroy ${deployName}: ${destroyedStack.stackStatus}`); + } + } catch (e: any) { + throw new Error(suffixWithErrors(formatErrorMessage(e), monitor?.errors)); + } finally { + if (monitor) { + await monitor.stop(); + } + } +} + +/** + * Checks whether we can skip deployment + * + * We do this in a complicated way by preprocessing (instead of just + * looking at the changeset), because if there are nested stacks involved + * the changeset will always show the nested stacks as needing to be + * updated, and the deployment will take a long time to in effect not + * do anything. + */ +async function canSkipDeploy( + deployStackOptions: DeployStackOptions, + cloudFormationStack: CloudFormationStack, + parameterChanges: ParameterChanges, +): Promise { + const deployName = deployStackOptions.deployName || deployStackOptions.stack.stackName; + debug(`${deployName}: checking if we can skip deploy`); + + // Forced deploy + if (deployStackOptions.force) { + debug(`${deployName}: forced deployment`); + return false; + } + + // Creating changeset only (default true), never skip + if ( + deployStackOptions.deploymentMethod?.method === 'change-set' && + deployStackOptions.deploymentMethod.execute === false + ) { + debug(`${deployName}: --no-execute, always creating change set`); + return false; + } + + // No existing stack + if (!cloudFormationStack.exists) { + debug(`${deployName}: no existing stack`); + return false; + } + + // Template has changed (assets taken into account here) + if (JSON.stringify(deployStackOptions.stack.template) !== JSON.stringify(await cloudFormationStack.template())) { + debug(`${deployName}: template has changed`); + return false; + } + + // Tags have changed + if (!compareTags(cloudFormationStack.tags, deployStackOptions.tags ?? [])) { + debug(`${deployName}: tags have changed`); + return false; + } + + // Notification arns have changed + if (!arrayEquals(cloudFormationStack.notificationArns, deployStackOptions.notificationArns ?? [])) { + debug(`${deployName}: notification arns have changed`); + return false; + } + + // Termination protection has been updated + if (!!deployStackOptions.stack.terminationProtection !== !!cloudFormationStack.terminationProtection) { + debug(`${deployName}: termination protection has been updated`); + return false; + } + + // Parameters have changed + if (parameterChanges) { + if (parameterChanges === 'ssm') { + debug(`${deployName}: some parameters come from SSM so we have to assume they may have changed`); + } else { + debug(`${deployName}: parameters have changed`); + } + return false; + } + + // Existing stack is in a failed state + if (cloudFormationStack.stackStatus.isFailure) { + debug(`${deployName}: stack is in a failure state`); + return false; + } + + // We can skip deploy + return true; +} + +/** + * Compares two list of tags, returns true if identical. + */ +function compareTags(a: Tag[], b: Tag[]): boolean { + if (a.length !== b.length) { + return false; + } + + for (const aTag of a) { + const bTag = b.find((tag) => tag.Key === aTag.Key); + + if (!bTag || bTag.Value !== aTag.Value) { + return false; + } + } + + return true; +} + +function suffixWithErrors(msg: string, errors?: string[]) { + return errors && errors.length > 0 ? `${msg}: ${errors.join(', ')}` : msg; +} + +function arrayEquals(a: any[], b: any[]): boolean { + return a.every((item) => b.includes(item)) && b.every((item) => a.includes(item)); +} + +function hasReplacement(cs: DescribeChangeSetCommandOutput) { + return (cs.Changes ?? []).some(c => { + const a = c.ResourceChange?.PolicyAction; + return a === 'ReplaceAndDelete' || a === 'ReplaceAndRetain' || a === 'ReplaceAndSnapshot'; + }); +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/deployments.ts b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/deployments.ts new file mode 100644 index 0000000000000..489f2e9239b69 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/lib/api/deployments.ts @@ -0,0 +1,801 @@ +import { randomUUID } from 'crypto'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as cdk_assets from 'cdk-assets'; +import { AssetManifest, IManifestEntry } from 'cdk-assets'; +import * as chalk from 'chalk'; +import type { SdkProvider } from './aws-auth/sdk-provider'; +import { type DeploymentMethod, deployStack, DeployStackResult, destroyStack } from './deploy-stack'; +import { EnvironmentAccess } from './environment-access'; +import { type EnvironmentResources } from './environment-resources'; +import { HotswapMode, HotswapPropertyOverrides } from './hotswap/common'; +import { debug, warning } from './logging'; +import { + loadCurrentTemplate, + loadCurrentTemplateWithNestedStacks, + type RootTemplateWithNestedStacks, +} from './nested-stack-helpers'; +import { DEFAULT_TOOLKIT_STACK_NAME } from './toolkit-info'; +import { determineAllowCrossAccountAssetPublishing } from './util/checks'; +import { + CloudFormationStack, + type ResourceIdentifierSummaries, + ResourcesToImport, + stabilizeStack, + Template, + uploadStackTemplateAssets, +} from './util/cloudformation'; +import { AssetManifestBuilder } from '../util/asset-manifest-builder'; +import { + buildAssets, + type BuildAssetsOptions, + EVENT_TO_LOGGER, + publishAssets, + type PublishAssetsOptions, + PublishingAws, +} from '../util/asset-publishing'; +import { StackActivityMonitor, StackActivityProgress } from './util/cloudformation/stack-activity-monitor'; +import { StackEventPoller } from './util/cloudformation/stack-event-poller'; +import { RollbackChoice } from './util/cloudformation/stack-status'; +import { makeBodyParameter } from './util/template-body-parameter'; +import { formatErrorMessage } from '../util/error'; + +const BOOTSTRAP_STACK_VERSION_FOR_ROLLBACK = 23; + +export interface Tag { + readonly Key: string; + readonly Value: string; +} + +export interface DeployStackOptions { + /** + * Stack to deploy + */ + readonly stack: cxapi.CloudFormationStackArtifact; + + /** + * Execution role for the deployment (pass through to CloudFormation) + * + * @default - Current role + */ + readonly roleArn?: string; + + /** + * Topic ARNs to send a message when deployment finishes (pass through to CloudFormation) + * + * @default - No notifications + */ + readonly notificationArns?: string[]; + + /** + * Override name under which stack will be deployed + * + * @default - Use artifact default + */ + readonly deployName?: string; + + /** + * Don't show stack deployment events, just wait + * + * @default false + */ + readonly quiet?: boolean; + + /** + * Name of the toolkit stack, if not the default name + * + * @default 'CDKToolkit' + */ + readonly toolkitStackName?: string; + + /** + * List of asset IDs which should NOT be built or uploaded + * + * @default - Build all assets + */ + readonly reuseAssets?: string[]; + + /** + * Stack tags (pass through to CloudFormation) + */ + readonly tags?: Tag[]; + + /** + * Stage the change set but don't execute it + * + * @default - true + * @deprecated Use 'deploymentMethod' instead + */ + readonly execute?: boolean; + + /** + * Optional name to use for the CloudFormation change set. + * If not provided, a name will be generated automatically. + * + * @deprecated Use 'deploymentMethod' instead + */ + readonly changeSetName?: string; + + /** + * Select the deployment method (direct or using a change set) + * + * @default - Change set with default options + */ + readonly deploymentMethod?: DeploymentMethod; + + /** + * Force deployment, even if the deployed template is identical to the one we are about to deploy. + * @default false deployment will be skipped if the template is identical + */ + readonly force?: boolean; + + /** + * Extra parameters for CloudFormation + * @default - no additional parameters will be passed to the template + */ + readonly parameters?: { [name: string]: string | undefined }; + + /** + * Use previous values for unspecified parameters + * + * If not set, all parameters must be specified for every deployment. + * + * @default true + */ + readonly usePreviousParameters?: boolean; + + /** + * Display mode for stack deployment progress. + * + * @default - StackActivityProgress.Bar - stack events will be displayed for + * the resource currently being deployed. + */ + readonly progress?: StackActivityProgress; + + /** + * Whether we are on a CI system + * + * @default false + */ + readonly ci?: boolean; + + /** + * Rollback failed deployments + * + * @default true + */ + readonly rollback?: boolean; + + /* + * Whether to perform a 'hotswap' deployment. + * A 'hotswap' deployment will attempt to short-circuit CloudFormation + * and update the affected resources like Lambda functions directly. + * + * @default - `HotswapMode.FULL_DEPLOYMENT` for regular deployments, `HotswapMode.HOTSWAP_ONLY` for 'watch' deployments + */ + readonly hotswap?: HotswapMode; + + /** + * Properties that configure hotswap behavior + */ + readonly hotswapPropertyOverrides?: HotswapPropertyOverrides; + + /** + * The extra string to append to the User-Agent header when performing AWS SDK calls. + * + * @default - nothing extra is appended to the User-Agent header + */ + readonly extraUserAgent?: string; + + /** + * List of existing resources to be IMPORTED into the stack, instead of being CREATED + */ + readonly resourcesToImport?: ResourcesToImport; + + /** + * If present, use this given template instead of the stored one + * + * @default - Use the stored template + */ + readonly overrideTemplate?: any; + + /** + * Whether to build/publish assets in parallel + * + * @default true To remain backward compatible. + */ + readonly assetParallelism?: boolean; + + /** + * Whether to deploy if the app contains no stacks. + * + * @default false + */ + ignoreNoStacks?: boolean; +} + +export interface RollbackStackOptions { + /** + * Stack to roll back + */ + readonly stack: cxapi.CloudFormationStackArtifact; + + /** + * Execution role for the deployment (pass through to CloudFormation) + * + * @default - Current role + */ + readonly roleArn?: string; + + /** + * Don't show stack deployment events, just wait + * + * @default false + */ + readonly quiet?: boolean; + + /** + * Whether we are on a CI system + * + * @default false + */ + readonly ci?: boolean; + + /** + * Name of the toolkit stack, if not the default name + * + * @default 'CDKToolkit' + */ + readonly toolkitStackName?: string; + + /** + * Whether to force a rollback or not + * + * Forcing a rollback will orphan all undeletable resources. + * + * @default false + */ + readonly force?: boolean; + + /** + * Orphan the resources with the given logical IDs + * + * @default - No orphaning + */ + readonly orphanLogicalIds?: string[]; + + /** + * Display mode for stack deployment progress. + * + * @default - StackActivityProgress.Bar - stack events will be displayed for + * the resource currently being deployed. + */ + readonly progress?: StackActivityProgress; + + /** + * Whether to validate the version of the bootstrap stack permissions + * + * @default true + */ + readonly validateBootstrapStackVersion?: boolean; +} + +export interface RollbackStackResult { + readonly notInRollbackableState?: boolean; + readonly success?: boolean; +} + +interface AssetOptions { + /** + * Stack with assets to build. + */ + readonly stack: cxapi.CloudFormationStackArtifact; + + /** + * Execution role for the building. + * + * @default - Current role + */ + readonly roleArn?: string; +} + +export interface BuildStackAssetsOptions extends AssetOptions { + /** + * Options to pass on to `buildAssets()` function + */ + readonly buildOptions?: BuildAssetsOptions; + + /** + * Stack name this asset is for + */ + readonly stackName?: string; +} + +interface PublishStackAssetsOptions extends AssetOptions { + /** + * Options to pass on to `publishAsests()` function + */ + readonly publishOptions?: Omit; + + /** + * Stack name this asset is for + */ + readonly stackName?: string; +} + +export interface DestroyStackOptions { + stack: cxapi.CloudFormationStackArtifact; + deployName?: string; + roleArn?: string; + quiet?: boolean; + force?: boolean; + ci?: boolean; +} + +export interface StackExistsOptions { + stack: cxapi.CloudFormationStackArtifact; + deployName?: string; + tryLookupRole?: boolean; +} + +export interface DeploymentsProps { + sdkProvider: SdkProvider; + readonly toolkitStackName?: string; + readonly quiet?: boolean; +} + +/** + * Scope for a single set of deployments from a set of Cloud Assembly Artifacts + * + * Manages lookup of SDKs, Bootstrap stacks, etc. + */ +export class Deployments { + public readonly envs: EnvironmentAccess; + + /** + * SDK provider for asset publishing (do not use for anything else). + * + * This SDK provider is only allowed to be used for that purpose, nothing else. + * + * It's not a different object, but the field name should imply that this + * object should not be used directly, except to pass to asset handling routines. + */ + private readonly assetSdkProvider: SdkProvider; + + /** + * SDK provider for passing to deployStack + * + * This SDK provider is only allowed to be used for that purpose, nothing else. + * + * It's not a different object, but the field name should imply that this + * object should not be used directly, except to pass to `deployStack`. + */ + private readonly deployStackSdkProvider: SdkProvider; + + private readonly publisherCache = new Map(); + + private _allowCrossAccountAssetPublishing: boolean | undefined; + constructor(private readonly props: DeploymentsProps) { + this.assetSdkProvider = props.sdkProvider; + this.deployStackSdkProvider = props.sdkProvider; + this.envs = new EnvironmentAccess(props.sdkProvider, props.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME); + } + + /** + * Resolves the environment for a stack. + */ + public async resolveEnvironment(stack: cxapi.CloudFormationStackArtifact): Promise { + return this.envs.resolveStackEnvironment(stack); + } + + public async readCurrentTemplateWithNestedStacks( + rootStackArtifact: cxapi.CloudFormationStackArtifact, + retrieveProcessedTemplate: boolean = false, + ): Promise { + const env = await this.envs.accessStackForLookupBestEffort(rootStackArtifact); + return loadCurrentTemplateWithNestedStacks(rootStackArtifact, env.sdk, retrieveProcessedTemplate); + } + + public async readCurrentTemplate(stackArtifact: cxapi.CloudFormationStackArtifact): Promise