diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 9b23e0194..c4eced062 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -33,4 +33,3 @@ jobs: fail_ci_if_error: true flags: suite.unit use_oidc: true - version: v10.3.0 diff --git a/.github/workflows/integ.yml b/.github/workflows/integ.yml index c18b7c696..247c0ef8a 100644 --- a/.github/workflows/integ.yml +++ b/.github/workflows/integ.yml @@ -89,7 +89,7 @@ jobs: - name: Create Verdaccio config run: |- mkdir -p $HOME/.config/verdaccio - echo '{"storage":"./storage","auth":{"htpasswd":{"file":"./htpasswd"}},"uplinks":{"npmjs":{"url":"https://registry.npmjs.org/"}},"packages":{"@aws-cdk/cloud-assembly-schema":{"access":"$all","publish":"$all","proxy":"npmjs"},"@aws-cdk/cloudformation-diff":{"access":"$all","publish":"$all","proxy":"none"},"cdk-assets":{"access":"$all","publish":"$all","proxy":"none"},"aws-cdk":{"access":"$all","publish":"$all","proxy":"none"},"@aws-cdk/cli-lib-alpha":{"access":"$all","publish":"$all","proxy":"none"},"cdk":{"access":"$all","publish":"$all","proxy":"none"},"**":{"access":"$all","proxy":"npmjs"}}}' > $HOME/.config/verdaccio/config.yaml + echo '{"storage":"./storage","auth":{"htpasswd":{"file":"./htpasswd"}},"uplinks":{"npmjs":{"url":"https://registry.npmjs.org/"}},"packages":{"@aws-cdk/cloud-assembly-schema":{"access":"$all","publish":"$all","proxy":"npmjs"},"@aws-cdk/cloudformation-diff":{"access":"$all","publish":"$all","proxy":"none"},"cdk-assets":{"access":"$all","publish":"$all","proxy":"none"},"aws-cdk":{"access":"$all","publish":"$all","proxy":"none"},"@aws-cdk/cli-lib-alpha":{"access":"$all","publish":"$all","proxy":"none"},"cdk":{"access":"$all","publish":"$all","proxy":"none"},"@aws-cdk-testing/cli-integ":{"access":"$all","publish":"$all","proxy":"none"},"**":{"access":"$all","proxy":"npmjs"}}}' > $HOME/.config/verdaccio/config.yaml - name: Start Verdaccio run: |- pm2 start verdaccio -- --config $HOME/.config/verdaccio/config.yaml @@ -100,7 +100,7 @@ jobs: echo 'registry=http://localhost:4873/' >> ~/.npmrc - name: Find an locally publish all tarballs run: |- - for pkg in packages/{@aws-cdk/cloud-assembly-schema,@aws-cdk/cloudformation-diff,cdk-assets,aws-cdk,@aws-cdk/cli-lib-alpha,cdk}/dist/js/*.tgz; do + for pkg in packages/{@aws-cdk/cloud-assembly-schema,@aws-cdk/cloudformation-diff,cdk-assets,aws-cdk,@aws-cdk/cli-lib-alpha,cdk,@aws-cdk-testing/cli-integ}/dist/js/*.tgz; do npm publish $pkg done - name: Download and install the test artifact diff --git a/.github/workflows/large-pr-checker.yml b/.github/workflows/large-pr-checker.yml index fe3462172..3d783c28a 100644 --- a/.github/workflows/large-pr-checker.yml +++ b/.github/workflows/large-pr-checker.yml @@ -30,6 +30,8 @@ jobs: | awk -F- '{print $NF}' \ | bc) + size=${size:-0} + echo "Total lines changed: $size" echo "total_lines_changed=$size" >> $GITHUB_OUTPUT - id: comment_pr diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d11c97d0b..5478946a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,6 +19,7 @@ jobs: publish-aws-cdk-cli-lib-alpha: ${{ steps.check-publish-aws-cdk-cli-lib-alpha.outputs.publish }} publish-cdk: ${{ steps.check-publish-cdk.outputs.publish }} publish-aws-cdk-integ-runner: ${{ steps.check-publish-aws-cdk-integ-runner.outputs.publish }} + publish-aws-cdk-testing-cli-integ: ${{ steps.check-publish-aws-cdk-testing-cli-integ.outputs.publish }} env: CI: "true" steps: @@ -65,6 +66,9 @@ jobs: - id: check-publish-aws-cdk-integ-runner run: (git ls-remote -q --exit-code --tags origin $(cat dist/releasetag.txt) && (echo "publish=false" >> $GITHUB_OUTPUT)) || echo "publish=true" >> $GITHUB_OUTPUT working-directory: packages/@aws-cdk/integ-runner + - id: check-publish-aws-cdk-testing-cli-integ + run: (git ls-remote -q --exit-code --tags origin $(cat dist/releasetag.txt) && (echo "publish=false" >> $GITHUB_OUTPUT)) || echo "publish=true" >> $GITHUB_OUTPUT + working-directory: packages/@aws-cdk-testing/cli-integ - name: Output the sha value that downstream checks expect id: git_remote run: echo "latest_commit=${{ github.sha }}" >> $GITHUB_OUTPUT @@ -176,6 +180,18 @@ jobs: name: aws-cdk-integ-runner_build-artifact path: packages/@aws-cdk/integ-runner/dist overwrite: true + - name: "@aws-cdk-testing/cli-integ: Backup artifact permissions" + if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} + run: cd dist && getfacl -R . > permissions-backup.acl + continue-on-error: true + working-directory: packages/@aws-cdk-testing/cli-integ + - name: "@aws-cdk-testing/cli-integ: Upload artifact" + if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} + uses: actions/upload-artifact@v4.4.0 + with: + name: aws-cdk-testing-cli-integ_build-artifact + path: packages/@aws-cdk-testing/cli-integ/dist + overwrite: true - name: "standalone: Upload artifact" if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} uses: actions/upload-artifact@v4.4.0 @@ -1050,6 +1066,60 @@ jobs: NPM_CONFIG_PROVENANCE: "true" NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: npx -p publib@latest publib-npm + aws-cdk-testing-cli-integ_release_github: + name: "@aws-cdk-testing/cli-integ: Publish to GitHub Releases" + needs: + - release + - aws-cdk-testing-cli-integ_release_npm + runs-on: ubuntu-latest + permissions: + contents: write + if: ${{ needs.release.outputs.latest_commit == github.sha && needs.release.outputs.publish-aws-cdk-testing-cli-integ == 'true' }} + steps: + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: aws-cdk-testing-cli-integ_build-artifact + path: dist + - name: Restore build artifact permissions + run: cd dist && setfacl --restore=permissions-backup.acl + continue-on-error: true + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + GITHUB_REF: ${{ github.sha }} + run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi + aws-cdk-testing-cli-integ_release_npm: + name: "@aws-cdk-testing/cli-integ: Publish to npm" + needs: release + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + if: ${{ needs.release.outputs.latest_commit == github.sha && needs.release.outputs.publish-aws-cdk-testing-cli-integ == 'true' }} + steps: + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: aws-cdk-testing-cli-integ_build-artifact + path: dist + - name: Restore build artifact permissions + run: cd dist && setfacl --restore=permissions-backup.acl + continue-on-error: true + - name: Release + env: + NPM_DIST_TAG: latest + NPM_REGISTRY: registry.npmjs.org + NPM_CONFIG_PROVENANCE: "true" + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npx -p publib@latest publib-npm standalone_release_adc: name: "standalone: publish to ADC" needs: release diff --git a/.projen/deps.json b/.projen/deps.json index 77d3b751c..eee36469b 100644 --- a/.projen/deps.json +++ b/.projen/deps.json @@ -109,6 +109,11 @@ "version": "30.0.0-alpha.7", "type": "override" }, + { + "name": "@jest/types", + "version": "30.0.0-alpha.7", + "type": "override" + }, { "name": "jest-environment-node", "version": "30.0.0-alpha.7", diff --git a/.projenrc.ts b/.projenrc.ts index 07c58d42a..7436feab1 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -13,6 +13,7 @@ import { LargePrChecker } from './projenrc/large-pr-checker'; import { PrLabeler } from './projenrc/pr-labeler'; import { RecordPublishingTimestamp } from './projenrc/record-publishing-timestamp'; import { DocType, S3DocsPublishing } from './projenrc/s3-docs-publishing'; +import { TypecheckTests } from './projenrc/TypecheckTests'; // 5.7 sometimes gives a weird error in `ts-jest` in `@aws-cdk/cli-lib-alpha` // https://github.com/microsoft/TypeScript/issues/60159 @@ -91,7 +92,7 @@ const CLI_SDK_V3_RANGE = '^3'; const defaultTsOptions: NonNullable['compilerOptions'] = { target: 'ES2020', module: 'commonjs', - lib: ['es2020', 'dom'], + lib: ['es2020'], incremental: true, esModuleInterop: false, skipLibCheck: true, @@ -270,6 +271,7 @@ const repoProject = new yarn.Monorepo({ repoProject.package.addPackageResolutions( 'jest-environment-node@30.0.0-alpha.7', '@jest/environment@30.0.0-alpha.7', + '@jest/types@30.0.0-alpha.7', ); new AdcPublishing(repoProject); @@ -345,7 +347,7 @@ function genericCdkProps(props: GenericProps = {}) { }, typescriptVersion: TYPESCRIPT_VERSION, checkLicenses: props.private ? undefined : { - allow: ['Apache-2.0', 'MIT', 'ISC'], + allow: ['Apache-2.0', 'MIT', 'ISC', 'BSD-3-Clause'], }, ...props, } satisfies Partial; @@ -654,6 +656,8 @@ const cdkAssets = configureProject( }), ); +new TypecheckTests(cdkAssets); + cdkAssets.addTask('shrinkwrap', { steps: [ { @@ -752,6 +756,12 @@ const tmpToolkitHelpers = configureProject( target: 'es2022', lib: ['es2022', 'esnext.disposable', 'dom'], module: 'NodeNext', + + // Temporarily, because it makes it impossible for us to use @internal functions + // from other packages. Annoyingly, VSCode won't show an error if you use @internal + // because it looks at the .ts, but the compilation will fail because it will use + // the .d.ts. + stripInternal: false, }, }, @@ -775,6 +785,8 @@ const tmpToolkitHelpers = configureProject( }), ); +new TypecheckTests(tmpToolkitHelpers); + // Prevent imports of private API surface tmpToolkitHelpers.package.addField('exports', { '.': './lib/index.js', @@ -924,6 +936,8 @@ const toolkitLib = configureProject( }), ); +new TypecheckTests(toolkitLib); + // TypeDoc documentation publishing new S3DocsPublishing(toolkitLib, { docsStream: 'toolkit-lib', @@ -1252,6 +1266,8 @@ const cli = configureProject( }), ); +new TypecheckTests(cli); + // Eslint rules cli.eslint?.addRules({ '@cdklabs/no-throw-default-error': 'error', @@ -1570,6 +1586,7 @@ const integRunner = configureProject( tsconfig: { compilerOptions: { ...defaultTsOptions, + lib: ['es2020', 'dom'], }, }, jestOptions: jestOptionsForProject({ @@ -1632,6 +1649,102 @@ new BundleCli(integRunner, { ////////////////////////////////////////////////////////////////////// +const cliInteg = configureProject( + new yarn.TypeScriptWorkspace({ + ...genericCdkProps(), + parent: repo, + name: '@aws-cdk-testing/cli-integ', + description: 'Integration tests for the AWS CDK CLI', + + // We set the majorVersion of this to 3.x, so that we can release + // it already without interfering with the current crop of CDK + // integ tests. + majorVersion: 3, + + srcdir: '.', + libdir: '.', + deps: [ + '@octokit/rest@^18.12.0', + `@aws-sdk/client-codeartifact@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-cloudformation@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-ecr@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-ecr-public@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-ecs@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-iam@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-lambda@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-s3@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-sns@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-sso@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/client-sts@${CLI_SDK_V3_RANGE}`, + `@aws-sdk/credential-providers@${CLI_SDK_V3_RANGE}`, + `@smithy/util-retry@${CLI_SDK_V3_RANGE}`, + `@smithy/types@${CLI_SDK_V3_RANGE}`, + '@cdklabs/cdk-atmosphere-client', + 'axios@^1', + 'chalk@^4', + 'fs-extra@^9', + 'glob@^7', + 'make-runnable@^1', + 'mockttp@^3', + 'npm@^10', + 'p-queue@^6', + 'semver@^7', + 'sinon@^9', + 'ts-mock-imports@^1', + 'yaml@1', + 'yargs@^17', + // Jest is a runtime dependency here! + 'jest@^29', + 'jest-junit@^15', + 'ts-jest@^29', + 'node-pty', + ], + devDeps: [ + '@types/semver@^7', + '@types/yargs@^15', + '@types/fs-extra@^9', + '@types/glob@^7', + ], + bin: { + 'run-suite': 'bin/run-suite', + 'download-and-run-old-tests': 'bin/download-and-run-old-tests', + 'query-github': 'bin/query-github', + 'apply-patches': 'bin/apply-patches', + 'test-root': 'bin/test-root', + 'stage-distribution': 'bin/stage-distribution', + }, + tsconfig: { + compilerOptions: { + ...defaultTsOptions, + esModuleInterop: false, + }, + include: ['**/*.ts'], + exclude: ['resources/**/*'], + }, + jestOptions: jestOptionsForProject({ + jestConfig: { + coverageThreshold: { + statements: 40, + lines: 40, + functions: 10, + branches: 40, + }, + }, + }), + }), +); +cliInteg.eslint?.addIgnorePattern('resources/**/*.ts'); + +const compiledDirs = ['tests', 'test', 'lib']; +for (const compiledDir of compiledDirs) { + cliInteg.gitignore.addPatterns(`${compiledDir}/**/*.js`); + cliInteg.gitignore.addPatterns(`${compiledDir}/**/*.d.ts`); +} +cliInteg.gitignore.addPatterns('!resources/**/*.js'); +cliInteg.npmignore?.addPatterns('!resources/**/*'); + +////////////////////////////////////////////////////////////////////// + // The pj.github.Dependabot component is only for a single Node project, // but we need multiple non-Node projects new pj.YamlFile(repo, '.github/dependabot.yml', { @@ -1671,6 +1784,7 @@ new CdkCliIntegTestsWorkflow(repo, { cli.name, cliLib.name, cdkAliasPackage.name, + cliInteg.name, ], allowUpstreamVersions: [ diff --git a/aws-cdk-cli.code-workspace b/aws-cdk-cli.code-workspace index 15a3a5841..230fa543d 100644 --- a/aws-cdk-cli.code-workspace +++ b/aws-cdk-cli.code-workspace @@ -5,6 +5,9 @@ "path": ".", "name": "" }, + { + "path": "packages/@aws-cdk-testing/cli-integ" + }, { "path": "packages/@aws-cdk/cdk-cli-wrapper" }, diff --git a/package.json b/package.json index 49c9b5812..c7878500f 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "eslint-plugin-jsdoc": "^50.6.9", "glob": "^11.0.1", "jest-junit": "^16", - "nx": "^20.7.2", + "nx": "^20.8.0", "prettier": "^2.8", "projen": "^0.91.20", "semver": "^7.7.1", @@ -47,6 +47,7 @@ }, "resolutions": { "@jest/environment": "30.0.0-alpha.7", + "@jest/types": "30.0.0-alpha.7", "jest-environment-node": "30.0.0-alpha.7" }, "engines": { @@ -74,7 +75,8 @@ "packages/@aws-cdk/cli-lib-alpha", "packages/@aws-cdk/cdk-cli-wrapper", "packages/cdk", - "packages/@aws-cdk/integ-runner" + "packages/@aws-cdk/integ-runner", + "packages/@aws-cdk-testing/cli-integ" ], "nohoist": [ "@aws-cdk/cloud-assembly-schema/jsonschema", @@ -97,7 +99,8 @@ "/packages/@aws-cdk/cli-lib-alpha", "/packages/@aws-cdk/cdk-cli-wrapper", "/packages/cdk", - "/packages/@aws-cdk/integ-runner" + "/packages/@aws-cdk/integ-runner", + "/packages/@aws-cdk-testing/cli-integ" ] }, "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." diff --git a/packages/@aws-cdk-testing/cli-integ/.eslintrc.js b/packages/@aws-cdk-testing/cli-integ/.eslintrc.js new file mode 100644 index 000000000..8f296a38a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/.eslintrc.js @@ -0,0 +1,9 @@ +var path = require('path'); +var fs = require('fs'); +var contents = fs.readFileSync(`${__dirname}/.eslintrc.json`, { encoding: 'utf-8' }); +// Strip comments, JSON.parse() doesn't like those +contents = contents.replace(/^\/\/.*$/m, ''); +var json = JSON.parse(contents); +// Patch the .json config with something that can only be represented in JS +json.parserOptions.tsconfigRootDir = __dirname; +module.exports = json; \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/.eslintrc.json b/packages/@aws-cdk-testing/cli-integ/.eslintrc.json new file mode 100644 index 000000000..2b9fb648e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/.eslintrc.json @@ -0,0 +1,321 @@ +// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". +{ + "env": { + "jest": true, + "node": true + }, + "root": true, + "plugins": [ + "@typescript-eslint", + "import", + "@cdklabs", + "@stylistic", + "jest", + "jsdoc" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "project": "./tsconfig.dev.json" + }, + "extends": [ + "plugin:import/typescript", + "plugin:jest/recommended", + "plugin:prettier/recommended" + ], + "settings": { + "import/parsers": { + "@typescript-eslint/parser": [ + ".ts", + ".tsx" + ] + }, + "import/resolver": { + "node": {}, + "typescript": { + "project": "./tsconfig.dev.json", + "alwaysTryTypes": true + } + } + }, + "ignorePatterns": [ + "*.js", + "*.d.ts", + "node_modules/", + "*.generated.ts", + "coverage", + "*.generated.ts", + "resources/**/*.ts" + ], + "rules": { + "@typescript-eslint/no-require-imports": [ + "error" + ], + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": [ + "**/test/**", + "**/build-tools/**" + ], + "optionalDependencies": false, + "peerDependencies": true + } + ], + "import/no-unresolved": [ + "error" + ], + "import/order": [ + "error", + { + "groups": [ + "builtin", + "external" + ], + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ], + "import/no-duplicates": "error", + "no-shadow": [ + "off" + ], + "@typescript-eslint/no-shadow": [ + "error" + ], + "key-spacing": [ + "error" + ], + "no-multiple-empty-lines": [ + "error", + { + "max": 1 + } + ], + "@typescript-eslint/no-floating-promises": [ + "error" + ], + "no-return-await": "off", + "@typescript-eslint/return-await": "error", + "no-trailing-spaces": [ + "error" + ], + "dot-notation": [ + "error" + ], + "no-bitwise": [ + "error" + ], + "@typescript-eslint/member-ordering": [ + "error", + { + "default": [ + "public-static-field", + "public-static-method", + "protected-static-field", + "protected-static-method", + "private-static-field", + "private-static-method", + "field", + "constructor", + "method" + ] + } + ], + "@cdklabs/no-core-construct": [ + "error" + ], + "@cdklabs/invalid-cfn-imports": [ + "error" + ], + "@cdklabs/no-literal-partition": [ + "error" + ], + "@cdklabs/no-invalid-path": [ + "error" + ], + "@cdklabs/promiseall-no-unbounded-parallelism": [ + "error" + ], + "no-throw-literal": [ + "error" + ], + "@stylistic/indent": [ + "error", + 2 + ], + "quotes": [ + "error", + "single", + { + "avoidEscape": true + } + ], + "@stylistic/member-delimiter-style": [ + "error" + ], + "@stylistic/comma-dangle": [ + "error", + "always-multiline" + ], + "@stylistic/no-extra-semi": [ + "error" + ], + "@stylistic/curly-newline": [ + "error", + "always" + ], + "comma-spacing": [ + "error", + { + "before": false, + "after": true + } + ], + "no-multi-spaces": [ + "error", + { + "ignoreEOLComments": false + } + ], + "array-bracket-spacing": [ + "error", + "never" + ], + "array-bracket-newline": [ + "error", + "consistent" + ], + "object-curly-spacing": [ + "error", + "always" + ], + "object-curly-newline": [ + "error", + { + "multiline": true, + "consistent": true + } + ], + "object-property-newline": [ + "error", + { + "allowAllPropertiesOnSameLine": true + } + ], + "keyword-spacing": [ + "error" + ], + "brace-style": [ + "error", + "1tbs", + { + "allowSingleLine": true + } + ], + "space-before-blocks": "error", + "curly": [ + "error", + "multi-line", + "consistent" + ], + "eol-last": [ + "error", + "always" + ], + "@stylistic/spaced-comment": [ + "error", + "always", + { + "exceptions": [ + "/", + "*" + ], + "markers": [ + "/" + ] + } + ], + "@stylistic/padded-blocks": [ + "error", + { + "classes": "never", + "blocks": "never", + "switches": "never" + } + ], + "jsdoc/require-param-description": [ + "error" + ], + "jsdoc/require-property-description": [ + "error" + ], + "jsdoc/require-returns-description": [ + "error" + ], + "jsdoc/check-alignment": [ + "error" + ], + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "punycode", + "message": "Package 'punycode' has to be imported with trailing slash, see warning in https://github.com/bestiejs/punycode.js#installation" + } + ], + "patterns": [ + "!punycode/" + ] + } + ], + "@typescript-eslint/consistent-type-imports": "error", + "semi": [ + "error", + "always" + ], + "quote-props": [ + "error", + "consistent-as-needed" + ], + "max-len": [ + "error", + { + "code": 150, + "ignoreUrls": true, + "ignoreStrings": true, + "ignoreTemplateLiterals": true, + "ignoreComments": true, + "ignoreRegExpLiterals": true + } + ], + "no-console": [ + "error" + ], + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression:matches([callee.name='createHash'], [callee.property.name='createHash']) Literal[value='md5']", + "message": "Use the md5hash() function from the core library if you want md5" + } + ], + "@typescript-eslint/unbound-method": "error", + "jest/expect-expect": "off", + "jest/no-conditional-expect": "off", + "jest/no-done-callback": "off", + "jest/no-standalone-expect": "off", + "jest/valid-expect": "off", + "jest/valid-title": "off", + "jest/no-identical-title": "off", + "jest/no-disabled-tests": "error", + "jest/no-focused-tests": "error", + "prettier/prettier": [ + "off" + ] + }, + "overrides": [] +} diff --git a/packages/@aws-cdk-testing/cli-integ/.gitattributes b/packages/@aws-cdk-testing/cli-integ/.gitattributes new file mode 100644 index 000000000..c1b26c9d0 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/.gitattributes @@ -0,0 +1,20 @@ +# ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". + +* text=auto eol=lf +/.eslintrc.js linguist-generated +/.eslintrc.json linguist-generated +/.gitattributes linguist-generated +/.gitignore linguist-generated +/.npmignore linguist-generated +/.prettierignore linguist-generated +/.prettierrc.json linguist-generated +/.projen/** linguist-generated +/.projen/deps.json linguist-generated +/.projen/files.json linguist-generated +/.projen/tasks.json linguist-generated +/jest.config.json linguist-generated +/LICENSE linguist-generated +/package.json linguist-generated +/tsconfig.dev.json linguist-generated +/tsconfig.json linguist-generated +/yarn.lock linguist-generated \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/.gitignore b/packages/@aws-cdk-testing/cli-integ/.gitignore new file mode 100644 index 000000000..8efff9c1b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/.gitignore @@ -0,0 +1,54 @@ +# ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". +!/.gitattributes +!/.projen/tasks.json +!/.projen/deps.json +!/.projen/files.json +!/package.json +!/LICENSE +!/.npmignore +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json +pids +*.pid +*.seed +*.pid.lock +lib-cov +coverage +*.lcov +.nyc_output +build/Release +node_modules/ +jspm_packages/ +*.tsbuildinfo +.eslintcache +*.tgz +.yarn-integrity +.cache +!/jest.config.json +/coverage/ +!/.prettierignore +!/.prettierrc.json +!/test/ +!/tsconfig.json +!/tsconfig.dev.json +!/./ +/./**/*.js +/./**/*.d.ts +/./**/*.d.ts.map +/dist/ +!/.eslintrc.json +/dist/changelog.md +/dist/version.txt +!/.eslintrc.js +tests/**/*.js +tests/**/*.d.ts +test/**/*.js +test/**/*.d.ts +lib/**/*.js +lib/**/*.d.ts +!resources/**/*.js diff --git a/packages/@aws-cdk-testing/cli-integ/.npmignore b/packages/@aws-cdk-testing/cli-integ/.npmignore new file mode 100644 index 000000000..5846e04a8 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/.npmignore @@ -0,0 +1,27 @@ +# ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". +/.projen/ +/jest.config.json +/coverage/ +/.prettierignore +/.prettierrc.json +/test/ +/tsconfig.dev.json +!/./ +!/./**/*.js +!/./**/*.d.ts +dist +/tsconfig.json +/.github/ +/.vscode/ +/.idea/ +/.projenrc.js +tsconfig.tsbuildinfo +/.eslintrc.json +/dist/changelog.md +/dist/version.txt +.eslintrc.js +*.ts +!*.d.ts +build-tools +!resources/**/* +/.gitattributes diff --git a/packages/@aws-cdk-testing/cli-integ/.prettierignore b/packages/@aws-cdk-testing/cli-integ/.prettierignore new file mode 100644 index 000000000..b6999ad11 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/.prettierignore @@ -0,0 +1,2 @@ +# ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". +.eslintrc.js diff --git a/packages/@aws-cdk-testing/cli-integ/.prettierrc.json b/packages/@aws-cdk-testing/cli-integ/.prettierrc.json new file mode 100644 index 000000000..af318ca5f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "printWidth": 120, + "singleQuote": true, + "trailingComma": "all", + "overrides": [] +} diff --git a/packages/@aws-cdk-testing/cli-integ/.projen/deps.json b/packages/@aws-cdk-testing/cli-integ/.projen/deps.json new file mode 100644 index 000000000..c84d31c98 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/.projen/deps.json @@ -0,0 +1,286 @@ +{ + "dependencies": [ + { + "name": "@cdklabs/eslint-plugin", + "type": "build" + }, + { + "name": "@stylistic/eslint-plugin", + "version": "^3", + "type": "build" + }, + { + "name": "@types/fs-extra", + "version": "^9", + "type": "build" + }, + { + "name": "@types/glob", + "version": "^7", + "type": "build" + }, + { + "name": "@types/jest", + "type": "build" + }, + { + "name": "@types/node", + "version": "^16", + "type": "build" + }, + { + "name": "@types/semver", + "version": "^7", + "type": "build" + }, + { + "name": "@types/yargs", + "version": "^15", + "type": "build" + }, + { + "name": "@typescript-eslint/eslint-plugin", + "version": "^8", + "type": "build" + }, + { + "name": "@typescript-eslint/parser", + "version": "^8", + "type": "build" + }, + { + "name": "commit-and-tag-version", + "version": "^12", + "type": "build" + }, + { + "name": "constructs", + "version": "^10.0.0", + "type": "build" + }, + { + "name": "eslint-config-prettier", + "type": "build" + }, + { + "name": "eslint-import-resolver-typescript", + "type": "build" + }, + { + "name": "eslint-plugin-import", + "type": "build" + }, + { + "name": "eslint-plugin-jest", + "type": "build" + }, + { + "name": "eslint-plugin-jsdoc", + "type": "build" + }, + { + "name": "eslint-plugin-prettier", + "type": "build" + }, + { + "name": "eslint", + "version": "^9", + "type": "build" + }, + { + "name": "jest", + "type": "build" + }, + { + "name": "jest-junit", + "version": "^16", + "type": "build" + }, + { + "name": "license-checker", + "type": "build" + }, + { + "name": "prettier", + "version": "^2.8", + "type": "build" + }, + { + "name": "projen", + "type": "build" + }, + { + "name": "ts-jest", + "type": "build" + }, + { + "name": "typescript", + "version": "5.6", + "type": "build" + }, + { + "name": "@aws-sdk/client-cloudformation", + "version": "^3", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-codeartifact", + "version": "^3", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-ecr-public", + "version": "^3", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-ecr", + "version": "^3", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-ecs", + "version": "^3", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-iam", + "version": "^3", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-lambda", + "version": "^3", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-s3", + "version": "^3", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-sns", + "version": "^3", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-sso", + "version": "^3", + "type": "runtime" + }, + { + "name": "@aws-sdk/client-sts", + "version": "^3", + "type": "runtime" + }, + { + "name": "@aws-sdk/credential-providers", + "version": "^3", + "type": "runtime" + }, + { + "name": "@cdklabs/cdk-atmosphere-client", + "type": "runtime" + }, + { + "name": "@octokit/rest", + "version": "^18.12.0", + "type": "runtime" + }, + { + "name": "@smithy/types", + "version": "^3", + "type": "runtime" + }, + { + "name": "@smithy/util-retry", + "version": "^3", + "type": "runtime" + }, + { + "name": "axios", + "version": "^1", + "type": "runtime" + }, + { + "name": "chalk", + "version": "^4", + "type": "runtime" + }, + { + "name": "fs-extra", + "version": "^9", + "type": "runtime" + }, + { + "name": "glob", + "version": "^7", + "type": "runtime" + }, + { + "name": "jest-junit", + "version": "^15", + "type": "runtime" + }, + { + "name": "jest", + "version": "^29", + "type": "runtime" + }, + { + "name": "make-runnable", + "version": "^1", + "type": "runtime" + }, + { + "name": "mockttp", + "version": "^3", + "type": "runtime" + }, + { + "name": "node-pty", + "type": "runtime" + }, + { + "name": "npm", + "version": "^10", + "type": "runtime" + }, + { + "name": "p-queue", + "version": "^6", + "type": "runtime" + }, + { + "name": "semver", + "version": "^7", + "type": "runtime" + }, + { + "name": "sinon", + "version": "^9", + "type": "runtime" + }, + { + "name": "ts-jest", + "version": "^29", + "type": "runtime" + }, + { + "name": "ts-mock-imports", + "version": "^1", + "type": "runtime" + }, + { + "name": "yaml", + "version": "1", + "type": "runtime" + }, + { + "name": "yargs", + "version": "^17", + "type": "runtime" + } + ], + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." +} diff --git a/packages/@aws-cdk-testing/cli-integ/.projen/files.json b/packages/@aws-cdk-testing/cli-integ/.projen/files.json new file mode 100644 index 000000000..493bbd87e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/.projen/files.json @@ -0,0 +1,19 @@ +{ + "files": [ + ".eslintrc.js", + ".eslintrc.json", + ".gitattributes", + ".gitignore", + ".npmignore", + ".prettierignore", + ".prettierrc.json", + ".projen/deps.json", + ".projen/files.json", + ".projen/tasks.json", + "jest.config.json", + "LICENSE", + "tsconfig.dev.json", + "tsconfig.json" + ], + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." +} diff --git a/packages/@aws-cdk-testing/cli-integ/.projen/tasks.json b/packages/@aws-cdk-testing/cli-integ/.projen/tasks.json new file mode 100644 index 000000000..351c28df3 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/.projen/tasks.json @@ -0,0 +1,222 @@ +{ + "tasks": { + "build": { + "name": "build", + "description": "Full release build", + "steps": [ + { + "spawn": "pre-compile" + }, + { + "spawn": "compile" + }, + { + "spawn": "post-compile" + }, + { + "spawn": "test" + }, + { + "spawn": "package" + } + ] + }, + "bump": { + "name": "bump", + "description": "Bumps version based on latest git tag and generates a changelog entry", + "env": { + "OUTFILE": "package.json", + "CHANGELOG": "dist/changelog.md", + "BUMPFILE": "dist/version.txt", + "RELEASETAG": "dist/releasetag.txt", + "RELEASE_TAG_PREFIX": "@aws-cdk-testing/cli-integ@", + "VERSIONRCOPTIONS": "{\"path\":\".\"}", + "BUMP_PACKAGE": "commit-and-tag-version@^12", + "RELEASABLE_COMMITS": "git log --no-merges --oneline $LATEST_TAG..HEAD -E --grep \"^(feat|fix){1}(\\([^()[:space:]]+\\))?(!)?:[[:blank:]]+.+\" -- .", + "MAJOR": "3" + }, + "steps": [ + { + "spawn": "gather-versions" + }, + { + "builtin": "release/bump-version" + } + ], + "condition": "git log --oneline -1 | grep -qv \"chore(release):\"" + }, + "check-for-updates": { + "name": "check-for-updates", + "env": { + "CI": "0" + }, + "steps": [ + { + "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --dep=dev,peer,prod,optional --filter=@cdklabs/eslint-plugin,@types/jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-jest,eslint-plugin-jsdoc,eslint-plugin-prettier,jest,license-checker,projen,ts-jest,@cdklabs/cdk-atmosphere-client,node-pty" + } + ] + }, + "check-licenses": { + "name": "check-licenses", + "steps": [ + { + "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC;BSD-3-Clause\"", + "receiveArgs": true + } + ] + }, + "compile": { + "name": "compile", + "description": "Only compile", + "steps": [ + { + "exec": "tsc --build", + "receiveArgs": true + } + ] + }, + "default": { + "name": "default", + "description": "Synthesize project files", + "steps": [ + { + "exec": "cd ../../.. && npx projen default" + } + ] + }, + "eslint": { + "name": "eslint", + "description": "Runs eslint against the codebase", + "env": { + "ESLINT_USE_FLAT_CONFIG": "false" + }, + "steps": [ + { + "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ . test build-tools", + "receiveArgs": true + } + ] + }, + "gather-versions": { + "name": "gather-versions", + "steps": [ + { + "exec": "node -e \"require(require.resolve('cdklabs-projen-project-types/lib/yarn/gather-versions.exec.js')).cliMain()\" ", + "receiveArgs": true + } + ] + }, + "install": { + "name": "install", + "description": "Install project dependencies and update lockfile (non-frozen)", + "steps": [ + { + "exec": "yarn install --check-files" + } + ] + }, + "install:ci": { + "name": "install:ci", + "description": "Install project dependencies using frozen lockfile", + "steps": [ + { + "exec": "yarn install --check-files --frozen-lockfile" + } + ] + }, + "nx": { + "name": "nx", + "steps": [ + { + "exec": "nx run", + "receiveArgs": true + } + ] + }, + "package": { + "name": "package", + "description": "Creates the distribution package", + "steps": [ + { + "exec": "mkdir -p dist/js" + }, + { + "exec": "npm pack --pack-destination dist/js" + } + ] + }, + "post-compile": { + "name": "post-compile", + "description": "Runs after successful compilation" + }, + "pre-compile": { + "name": "pre-compile", + "description": "Prepare the project for compilation", + "steps": [ + { + "spawn": "check-licenses" + } + ] + }, + "test": { + "name": "test", + "description": "Run tests", + "steps": [ + { + "exec": "jest --passWithNoTests --updateSnapshot", + "receiveArgs": true + }, + { + "spawn": "eslint" + } + ] + }, + "test:watch": { + "name": "test:watch", + "description": "Run jest in watch mode", + "steps": [ + { + "exec": "jest --watch" + } + ] + }, + "unbump": { + "name": "unbump", + "description": "Restores version to 0.0.0", + "env": { + "OUTFILE": "package.json", + "CHANGELOG": "dist/changelog.md", + "BUMPFILE": "dist/version.txt", + "RELEASETAG": "dist/releasetag.txt", + "RELEASE_TAG_PREFIX": "@aws-cdk-testing/cli-integ@", + "VERSIONRCOPTIONS": "{\"path\":\".\"}", + "BUMP_PACKAGE": "commit-and-tag-version@^12", + "RELEASABLE_COMMITS": "git log --no-merges --oneline $LATEST_TAG..HEAD -E --grep \"^(feat|fix){1}(\\([^()[:space:]]+\\))?(!)?:[[:blank:]]+.+\" -- ." + }, + "steps": [ + { + "builtin": "release/reset-version" + }, + { + "spawn": "gather-versions", + "env": { + "RESET_VERSIONS": "true" + } + } + ] + }, + "watch": { + "name": "watch", + "description": "Watch & compile in the background", + "steps": [ + { + "exec": "tsc --build -w" + } + ] + } + }, + "env": { + "PATH": "$(npx -c \"node --print process.env.PATH\")" + }, + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." +} diff --git a/packages/@aws-cdk-testing/cli-integ/LICENSE b/packages/@aws-cdk-testing/cli-integ/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/LICENSE @@ -0,0 +1,202 @@ + + 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 [yyyy] [name of copyright owner] + + 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-testing/cli-integ/NOTICE b/packages/@aws-cdk-testing/cli-integ/NOTICE new file mode 100644 index 000000000..62c4308b0 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/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-testing/cli-integ/README.md b/packages/@aws-cdk-testing/cli-integ/README.md new file mode 100644 index 000000000..d1dd48566 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/README.md @@ -0,0 +1,205 @@ +# CDK CLI integration test + + +--- + +![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge) + +--- + + + +This package contains CDK CLI integration test suites, as well as helper tools necessary to run those suites against various different distributions of the CDK (in the source repository, against a build directory, or against published releases). + +> The tools and tests themselves should arguably be in different packages, but I want to prevent package proliferation. For now we'll keep them together. + +## Tests + +The tests themselves are in the `tests/` directory: + +```text +tests/ +├── cli-integ-tests +├── init-csharp +├── init-fsharp +├── init-java +├── init-javascript +├── init-python +├── init-typescript-app +├── init-typescript-lib +├── init-go +└── uberpackage +``` + +Each subdirectory contains one test **suite**, and in the development pipeline each suite is run individually in a CodeBuild job, all in parallel. This requires manual configuration in the pipeline: to add a new suite, first add a suite here, then add the suite to the pipeline as well. The safest strategy is to add a trivially succeeding suite first (for example, a single test with `expect(true).toBeTruthy()`), add it to the pipeline, and then write the actual tests. + +Test suites are written as a collection of Jest tests, and they are run using Jest, using the code in the `lib/` directory as helpers. + +### Setup + +Building the @aws-cdk-testing package is not very different from building the rest of the CDK. However, If you are having issues with the tests, you can ensure your environment is built properly by following the steps below: + +```shell +yarn install # Install dependencies +npx lerna run build --scope=aws-cdk # Build the CDK cli +yarn build # Build the @aws-cdk-testing/cli-integ package +../../../scripts/align-version.sh # Align the versions of CDK packages +``` + +### Running tests with debugger + +```json +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "args": ["-a", "cli-integ-tests", "-t", "context in stage propagates to top"], + "name": "debug integ tests", + "program": "~/aws-cdk/packages/@aws-cdk-testing/cli-integ/bin/run-suite", + "console": "integratedTerminal", + "sourceMaps": true, + "skipFiles": [ "/**/*" ], + "stopOnEntry": false + } + ] +} +``` + +1. Assuming you checked out the `aws-cdk` repository in your `~` directory, use the above `launch.json`. +2. In the `"args"` value after `"-t"`, place the name of the test that you'd like to run. +3. Press the VS code green arrow to launch the debugger. + +### Running a test suite + +You run a suite using the `bin/run-suite` tool. You must select either a version of the CLI and framework which can be `npm install`ed, or point to the root of the source tree: + +```shell +# Use the given source tree +$ bin/run-suite --use-source=/path/to/repo-root + +# Automatically determine the source tree root +$ bin/run-suite -a + +# Run against a released version +$ bin/run-suite --use-cli-release=2.34.5 +``` + +To run a specific test, add `-t` and a substring of the test name. For example: + +```shell +bin/run-suite -a cli-integ-tests -t 'load old assemblies' +``` + +### Running a test suite against binaries + +Some test suites require package binaries stages in CodeArtifact repositories to run. This requires you to do a full build, then create a CodeArtifact repository in your own account, uploading the packages there, and then running the tests in a shell configured to have NPM, Pip, Maven etc look for those packages in CodeArtifact. + +```shell +# Build and pack all of CDK (will take ~an hour) +$ ./build.sh +$ ./pack.sh + +# Use publib to upload to CodeArtifact +$ npm install -g publib +# publib-ca is a CLI tool that comes with publib +$ publib-ca create +$ publib-ca publish /path/to/dist + +# Run the tests against those repositories (may need to substitute 0.0.0 w/ local number) +$ source ~/.publib-ca/usage/activate.bash +$ bin/run-suite --use-cli-release=0.0.0 + +# Clean up +$ publib-ca delete +``` + +## Tools + +There are a number of tools in the `bin/` directory. They are: + +```text +bin/ +├── apply-patches +├── query-github +├── run-suite +├── stage-distribution +└── test-root +``` + +* `apply-patches`: used for regression testing. Applies patches to historical versions of the tests to fix false positive test failures. +* `query-github`: used for regression testing. Queries GitHub for previously released versions. +* `run-suite`: run one of the test suites in the `tests/` directory. +* `stage-distribution`: used for testing in the pipeline. Uploads candidate release binaries to CodeArtifact so that they can be installed using `npm install`, `pip install`, etc. +* `test-root`: return the directory containing all tests (used for applying patches). + +## Regression testing + +The regression testing mechanism is somewhat involved and therefore deserves its own section. The principle is not too hard to explain though: + +*We run the previous version of the CLI integ tests against the new candidate release of the CLI, to make sure we didn't accidentally introduce any breaking behavior*. + +This is slightly complicated by two facts: + +* (1) Both the CLI and the framework may have changed, and an incompatibility may have arisen between the framework and CLI. Newer CLIs must always support older framework versions. We therefore run two flavors of the integration tests: + * Old tests, new CLI, new framework + * Old tests, new CLI, old framework + +The testing matrix looks like this: + +```text + OLD TESTS NEW TESTS + + CLI CLI + Old New Old New + ┌────────┬────────┐ ┌────────┬────────┐ + F'WORK │ prev │ │ F'WORK │ │ │ + Old │ rls │ regr │ Old │ (N/A) │ ? │ + │ integ │ │ │ │ │ + ├────────┼────────┤ ├────────┼────────┤ + │ │ │ │ │ cur │ + New │ (N/A) │ regr │ New │ (N/A) │ rls │ + │ │ │ │ │ integ │ + └────────┴────────┘ └────────┴────────┘ +``` + +We are covering everything except "new tests, new CLI, old framework", which is not clear that it even makes sense to test because some new features may rely on framework support which will not be in the old version yet. + +* (2) Sometimes, old tests will fail on newer releases when we introduce breaking changes to the framework or CLI for something serious (such as security reasons), or maybe because we had a bug in an old version that happened to pass, but now the test needs to be updated in order to pass a bugfix. + +For this case we have a patching mechanism, so that in a NEW release of the tools, we include files that are copied over an OLD release of the test, that allows them to pass. For the simplest case there is a mechanism to suppress the run of a single test, so that we can skip the running of one test for one release. For more complicated cases we copy in patched `.js` source files which will replace old source files. (Patches are considered part of the *tools*, not part of the *tests*). + +### Mechanism + +To run the tests in a regressory fashion, do the following: + +* Download the current `@aws-cdk-testing/cli-integ` artifact at `V1`. +* Determine the previous version `V0` (use `query-github` for this). +* Download the previous `@aws-cdk-testing/cli-integ` artifact at `V0`. +* From the `V1` artifact, apply the `V0` patch set. +* Run the `V0` tests with the `--framework-version` option: + +```shell +# Old tests, new CLI, new framework +V0/bin/run-suite --use-cli-release=V1 --framework-version=V1 [...] + +# Old tests, new CLI, old framework +V0/bin/run-suite --use-cli-release=V1 --framework-version=V0 [...] +``` + +### Patching + +To patch a previous set of tests to make them pass with a new release, add a directory to `resources/cli-regression-patches`. The simplest method is to add a `skip-tests.txt` file: + +```shell +# The version of the tests that are currently failing (V0 in the paragraph above) +export VERSION=X.Y.Z + +mkdir -p resources/cli-regression-patches/v${VERSION} +cp skip-tests.txt resources/cli-regression-patches/v${VERSION}/ +``` + +Now edit `resources/cli-regression-patches/vX.Y.Z/skip-tests.txt` and put the name of the test you want to skip on a line by itself. + +If you need to replace source files, it's probably best to stick compiled `.js` files in here. `.ts` source files wouldn't compile because they'd be missing `imports`. diff --git a/packages/@aws-cdk-testing/cli-integ/bin/apply-patches b/packages/@aws-cdk-testing/cli-integ/bin/apply-patches new file mode 100755 index 000000000..302296253 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/bin/apply-patches @@ -0,0 +1,22 @@ +#!/bin/bash +# Written in bash just because that's easier with all the file manipulation +set -eu +scriptdir=$(cd $(dirname $0) && pwd) +version="$1" +target_dir="$2" + + +if [[ ! -f "$2/skip-tests.txt" ]]; then + echo "$2: does not look like a test root directory." >&2 + exit 1 +fi + + +candidate_dir="${scriptdir}/../resources/cli-regression-patches/v${version}" + +if [[ -d "$candidate_dir" ]]; then + echo "Found patch directory: ${candidate_dir}" + cp -vR "${candidate_dir}/"* "$2" +else + echo "No patch directory named: ${candidate_dir}" +fi diff --git a/packages/@aws-cdk-testing/cli-integ/bin/download-and-run-old-tests b/packages/@aws-cdk-testing/cli-integ/bin/download-and-run-old-tests new file mode 100755 index 000000000..ad72ce50c --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/bin/download-and-run-old-tests @@ -0,0 +1,52 @@ +#!/bin/bash +set -eu +# Download old tests and run them. Written in bash, just because. Needs to do contortions, see below. +# +# Usage: +# +# download-and-run-old-tests [...args to run-suite...] +set -x + +scriptdir=$(cd $(dirname $0) && pwd) + +version="$1" +target_directory="old_tests" +shift + +rm -rf $target_directory && mkdir $target_directory + +# The old tests package MUST be 'npm install 'ed as a dependency, but MUST NOT +# end up in a `node_modules` directory. +# +# - MUST be 'npm install'ed: we need transitive dependencies as well. +# - as a dependency: if we check out the source and do an `npm install --production` in the +# package.json directory, NPM will still try to resolve devDependencies (even though it doesn't +# need to install them), and the devDeps do not exist on npmjs. +# - MUST NOT end up in `node_modules`: Jest 27 will ignore all tests that have `node_modules` in +# the path, and this behavior is not configurable before Jest 28. Unfortunately, because of TypeScript +# typing issues, we cannot move past Jest 27. +# +# To achieve this, do an `npm install ` then follow up with an `mv` to move the files out. +if ! npm install --prefix $target_directory --no-save @aws-cdk-testing/cli-integ@$version > npm.log 2>&1; then + cat npm.log >&2 + # Catch a "package does not exist" error, have to do it this way because for some reason, + # 'npm view ' doesn't exit with an error... :s + if grep -q 'code ETARGET' npm.log; then + echo "During migration, @aws-cdk-testing/cli-integ@$version does not exist yet." >&2 + + + # Do create an empty junit.xml file -- if we don't, then the "upload report" phase will fail + # if there are 0 files to upload. + echo '' > junit.xml + exit 0 + fi + exit 1 +fi + +mv $($target_directory/node_modules/.bin/test-root)/* $target_directory + +# Apply new patches to old tests +${scriptdir}/apply-patches $version $target_directory + +# Run the suite from the old tests +exec $target_directory/bin/run-suite "$@" \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/bin/query-github b/packages/@aws-cdk-testing/cli-integ/bin/query-github new file mode 100755 index 000000000..34c2f861a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/bin/query-github @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../lib/cli/query-github.js'); diff --git a/packages/@aws-cdk-testing/cli-integ/bin/run-suite b/packages/@aws-cdk-testing/cli-integ/bin/run-suite new file mode 100755 index 000000000..8b2b678b9 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/bin/run-suite @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../lib/cli/run-suite.js'); \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/bin/stage-distribution b/packages/@aws-cdk-testing/cli-integ/bin/stage-distribution new file mode 100755 index 000000000..5105f5fd7 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/bin/stage-distribution @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../lib/cli/stage-distribution.js'); diff --git a/packages/@aws-cdk-testing/cli-integ/bin/test-root b/packages/@aws-cdk-testing/cli-integ/bin/test-root new file mode 100755 index 000000000..9cb8f0f22 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/bin/test-root @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../lib/cli/test-root.js'); diff --git a/packages/@aws-cdk-testing/cli-integ/entrypoints/test-cli-regression-against-current-code.sh b/packages/@aws-cdk-testing/cli-integ/entrypoints/test-cli-regression-against-current-code.sh new file mode 100755 index 000000000..ff50aa5f6 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/entrypoints/test-cli-regression-against-current-code.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# +# Run our integration tests in regression mode against the +# candidate version of the framework, which is the one we just packed. +# +set -euo pipefail +integdir=$(cd $(dirname $0) && pwd) + +source ${integdir}/test-cli-regression.bash + +run_regression_against_framework_version CANDIDATE_VERSION diff --git a/packages/@aws-cdk-testing/cli-integ/entrypoints/test-cli-regression-against-latest-release.sh b/packages/@aws-cdk-testing/cli-integ/entrypoints/test-cli-regression-against-latest-release.sh new file mode 100755 index 000000000..8b670eb7b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/entrypoints/test-cli-regression-against-latest-release.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# +# Run our integration tests in regression mode against the +# previous version of the framework, relative to the version being packed now. +# +set -euo pipefail +integdir=$(cd $(dirname $0) && pwd) + +source ${integdir}/test-cli-regression.bash + +run_regression_against_framework_version PREVIOUS_VERSION diff --git a/packages/@aws-cdk-testing/cli-integ/entrypoints/test-cli-regression.bash b/packages/@aws-cdk-testing/cli-integ/entrypoints/test-cli-regression.bash new file mode 100644 index 000000000..1770ee269 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/entrypoints/test-cli-regression.bash @@ -0,0 +1,83 @@ +#!/bin/bash +# +# Helper functions for CLI regression tests. +# +set -euo pipefail +integdir=$(cd $(dirname $0) && pwd) + +# Run our integration tests in regression mode. +# +# 1. Figure out what was the previous (relative to the current candidate) version we published. +# 2. Download the integration tests artifact from that version. +# 2. Copy its integration tests directory ((test/integ/cli)) here. +# 3. Run the integration tests from the copied directory. +# +# Positional Arugments: +# +# 1) Framework version identifier. Which version of the framework should the tests run against. Options are: +# +# - CANDIDATE_VERSION: Use the candidate code, i.e the one being built right now. +# - PREVIOUS_VERSION: Use the previous version code, i.e the published version prior to CANDIDATE_VERSION. +# +function run_regression_against_framework_version() { + + TEST_RUNNER=${TEST_RUNNER:-""} + CANDIDATE_VERSION=${CANDIDATE_VERSION:-""} + + if [ "${TEST_RUNNER}" != "dist" ]; then + echo "Unsupported runner: ${TEST_RUNNER}. Regression tests can only run with the 'dist' runner" + exit 1 + fi + + SUPPORTED_FRAMEWORK_VERSION_IDENTIFIERS=("CANDIDATE_VERSION PREVIOUS_VERSION") + FRAMEWORK_VERSION_IDENTIFIER=$1 + if [[ ! " ${SUPPORTED_FRAMEWORK_VERSION_IDENTIFIERS[@]} " =~ " ${FRAMEWORK_VERSION_IDENTIFIER} " ]]; then + echo "Unsupported framework version identifier. Should be one of ${SUPPORTED_FRAMEWORK_VERSION_IDENTIFIERS}" + exit 1 + fi + + echo "Fetching previous version for candidate: ${CANDIDATE_VERSION}" + + # we need to explicitly install these deps because this script is executed + # int the test phase, which means the cwd is the packaged dist directory, + # so it doesn't have the dependencies installed from the installation of the package.json + # in the build phase. maybe we should just run npm install on the package.json again? + npm install @octokit/rest@^18.0.6 semver@^7.3.2 make-runnable@^1.3.8 + PREVIOUS_VERSION=$(node ${integdir}/github-helpers.js fetchPreviousVersion ${CANDIDATE_VERSION}) + + echo "Previous version is: ${PREVIOUS_VERSION}" + + temp_dir=$(mktemp -d) + integ_under_test=${integdir}/cli-backwards-tests-${PREVIOUS_VERSION} + + pushd ${temp_dir} + + echo "Downloading aws-cdk ${PREVIOUS_VERSION} tarball from npm" + npm pack aws-cdk@${PREVIOUS_VERSION} + tar -zxf aws-cdk-${PREVIOUS_VERSION}.tgz + + rm -rf ${integ_under_test} + + echo "Copying integration tests of version ${PREVIOUS_VERSION} to ${integ_under_test} (dont worry, its gitignored)" + cp -r ${temp_dir}/package/test/integ/cli "${integ_under_test}" + cp -r ${temp_dir}/package/test/integ/helpers "${integ_under_test}" + + patch_dir="${integdir}/cli-regression-patches/v${PREVIOUS_VERSION}" + # delete possibly stale junit.xml file + rm -f ${integ_under_test}/junit.xml + if [[ -d "$patch_dir" ]]; then + echo "Hotpatching the tests with files from $patch_dir" >&2 + cp -r "$patch_dir"/* ${integ_under_test} + fi + + popd + + # the framework version to use is determined by the caller as the first argument. + # its a variable name indirection. + export FRAMEWORK_VERSION=${!FRAMEWORK_VERSION_IDENTIFIER} + + # Show the versions we settled on + echo "♈️ Regression testing [cli $(cdk --version)] against [framework ${FRAMEWORK_VERSION}] using [tests ${PREVIOUS_VERSION}}]" + + ${integ_under_test}/test.sh +} diff --git a/packages/@aws-cdk-testing/cli-integ/entrypoints/test.sh b/packages/@aws-cdk-testing/cli-integ/entrypoints/test.sh new file mode 100755 index 000000000..bf0ec0a7c --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/entrypoints/test.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -euo pipefail +scriptdir=$(cd $(dirname $0) && pwd) + +echo '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' +echo 'CLI Integration Tests' +echo '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' + +cd $scriptdir + +source ../common/jest-test.bash +invokeJest "$@" diff --git a/packages/@aws-cdk-testing/cli-integ/jest.config.json b/packages/@aws-cdk-testing/cli-integ/jest.config.json new file mode 100644 index 000000000..2525df222 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/jest.config.json @@ -0,0 +1,68 @@ +{ + "coverageProvider": "v8", + "moduleFileExtensions": [ + "ts", + "js" + ], + "maxWorkers": "80%", + "testEnvironment": "node", + "coverageThreshold": { + "global": { + "statements": 40, + "branches": 40, + "functions": 10, + "lines": 40 + } + }, + "collectCoverage": true, + "coverageReporters": [ + "text-summary", + "cobertura", + [ + "html", + { + "subdir": "html-report" + } + ] + ], + "testMatch": [ + "/test/**/?(*.)+(test).ts", + "/@(.|test)/**/*(*.)@(spec|test).ts?(x)", + "/@(.|test)/**/__tests__/**/*.ts?(x)" + ], + "coveragePathIgnorePatterns": [ + "\\.generated\\.[jt]s$", + "/test/", + ".warnings.jsii.js$", + "/node_modules/" + ], + "reporters": [ + "default", + [ + "jest-junit", + { + "suiteName": "jest tests", + "outputDirectory": "coverage" + } + ] + ], + "randomize": true, + "clearMocks": true, + "coverageDirectory": "coverage", + "testPathIgnorePatterns": [ + "/node_modules/" + ], + "watchPathIgnorePatterns": [ + "/node_modules/" + ], + "transform": { + "^.+\\.[t]sx?$": [ + "ts-jest", + { + "tsconfig": "tsconfig.dev.json", + "isolatedModules": true + } + ] + }, + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/aws.ts b/packages/@aws-cdk-testing/cli-integ/lib/aws.ts new file mode 100644 index 000000000..0bd882510 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/aws.ts @@ -0,0 +1,303 @@ +import { + CloudFormationClient, + DeleteStackCommand, + DescribeStacksCommand, + UpdateTerminationProtectionCommand, + type Stack, +} from '@aws-sdk/client-cloudformation'; +import { DeleteRepositoryCommand, ECRClient } from '@aws-sdk/client-ecr'; +import { ECRPUBLICClient } from '@aws-sdk/client-ecr-public'; +import { ECSClient } from '@aws-sdk/client-ecs'; +import { IAMClient } from '@aws-sdk/client-iam'; +import { LambdaClient } from '@aws-sdk/client-lambda'; +import { + S3Client, + DeleteObjectsCommand, + ListObjectVersionsCommand, + type ObjectIdentifier, + DeleteBucketCommand, +} from '@aws-sdk/client-s3'; +import { SNSClient } from '@aws-sdk/client-sns'; +import { SSOClient } from '@aws-sdk/client-sso'; +import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; +import { fromIni, fromNodeProviderChain } from '@aws-sdk/credential-providers'; +import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@smithy/types'; +import { ConfiguredRetryStrategy } from '@smithy/util-retry'; +interface ClientConfig { + readonly credentials: AwsCredentialIdentityProvider | AwsCredentialIdentity; + readonly region: string; + readonly retryStrategy: ConfiguredRetryStrategy; +} + +export class AwsClients { + public static async forIdentity(region: string, identity: AwsCredentialIdentity, output: NodeJS.WritableStream) { + return new AwsClients(region, output, identity); + } + + public static async forRegion(region: string, output: NodeJS.WritableStream) { + return new AwsClients(region, output); + } + + private readonly config: ClientConfig; + + public readonly cloudFormation: CloudFormationClient; + public readonly s3: S3Client; + public readonly ecr: ECRClient; + public readonly ecrPublic: ECRPUBLICClient; + public readonly ecs: ECSClient; + public readonly sso: SSOClient; + public readonly sns: SNSClient; + public readonly iam: IAMClient; + public readonly lambda: LambdaClient; + public readonly sts: STSClient; + + constructor( + public readonly region: string, + private readonly output: NodeJS.WritableStream, + public readonly identity?: AwsCredentialIdentity) { + this.config = { + credentials: this.identity ?? chainableCredentials(this.region), + region: this.region, + retryStrategy: new ConfiguredRetryStrategy(9, (attempt: number) => attempt ** 500), + }; + this.cloudFormation = new CloudFormationClient(this.config); + this.s3 = new S3Client(this.config); + this.ecr = new ECRClient(this.config); + this.ecrPublic = new ECRPUBLICClient({ ...this.config, region: 'us-east-1' /* public gallery is only available in us-east-1 */ }); + this.ecs = new ECSClient(this.config); + this.sso = new SSOClient(this.config); + this.sns = new SNSClient(this.config); + this.iam = new IAMClient(this.config); + this.lambda = new LambdaClient(this.config); + this.sts = new STSClient(this.config); + } + + public async account(): Promise { + // Reduce # of retries, we use this as a circuit breaker for detecting no-config + const stsClient = new STSClient({ + credentials: this.config.credentials, + region: this.config.region, + maxAttempts: 2, + }); + + return (await stsClient.send(new GetCallerIdentityCommand({}))).Account!; + } + + /** + * If the clients already has an established identity (via atmosphere for example), + * return an environment variable map activating it. + * + * Otherwise, returns undefined. + */ + public identityEnv(): Record | undefined { + return this.identity ? { + AWS_ACCESS_KEY_ID: this.identity.accessKeyId, + AWS_SECRET_ACCESS_KEY: this.identity.secretAccessKey, + AWS_SESSION_TOKEN: this.identity.sessionToken!, + + // unset any previously used profile because the SDK will prefer + // this over static env credentials. this is relevant for tests running on CodeBuild + // because we use a profile as our main credentials source. + AWS_PROFILE: '', + } : undefined; + } + + /** + * Resolve the current identity or identity provider to credentials + */ + public async credentials() { + const x = this.config.credentials; + if (isAwsCredentialIdentity(x)) { + return x; + } + return x(); + } + + public async deleteStacks(...stackNames: string[]) { + if (stackNames.length === 0) { + return; + } + + // We purposely do all stacks serially, because they've been ordered + // to do the bootstrap stack last. + for (const stackName of stackNames) { + await this.cloudFormation.send( + new UpdateTerminationProtectionCommand({ + EnableTerminationProtection: false, + StackName: stackName, + }), + ); + await this.cloudFormation.send( + new DeleteStackCommand({ + StackName: stackName, + }), + ); + + await retry(this.output, `Deleting ${stackName}`, retry.forSeconds(600), async () => { + const status = await this.stackStatus(stackName); + if (status !== undefined && status.endsWith('_FAILED')) { + throw retry.abort(new Error(`'${stackName}' is in state '${status}'`)); + } + if (status !== undefined) { + throw new Error(`Delete of '${stackName}' not complete yet, status: '${status}'`); + } + }); + } + } + + public async stackStatus(stackName: string): Promise { + try { + return ( + await this.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackName, + }), + ) + ).Stacks?.[0].StackStatus; + } catch (e: any) { + if (isStackMissingError(e)) { + return undefined; + } + throw e; + } + } + + public async emptyBucket(bucketName: string, options?: { bypassGovernance?: boolean }) { + const objects = await this.s3.send( + new ListObjectVersionsCommand({ + Bucket: bucketName, + }), + ); + + const deletes = [...(objects.Versions || []), ...(objects.DeleteMarkers || [])].reduce((acc, obj) => { + if (typeof obj.VersionId !== 'undefined' && typeof obj.Key !== 'undefined') { + acc.push({ Key: obj.Key, VersionId: obj.VersionId }); + } else if (typeof obj.Key !== 'undefined') { + acc.push({ Key: obj.Key }); + } + return acc; + }, [] as ObjectIdentifier[]); + + if (deletes.length === 0) { + return Promise.resolve(); + } + + return this.s3.send( + new DeleteObjectsCommand({ + Bucket: bucketName, + Delete: { + Objects: deletes, + Quiet: false, + }, + BypassGovernanceRetention: options?.bypassGovernance ? true : undefined, + }), + ); + } + + public async deleteImageRepository(repositoryName: string) { + await this.ecr.send( + new DeleteRepositoryCommand({ + repositoryName: repositoryName, + force: true, + }), + ); + } + + public async deleteBucket(bucketName: string) { + try { + await this.emptyBucket(bucketName); + + await this.s3.send( + new DeleteBucketCommand({ + Bucket: bucketName, + }), + ); + } catch (e: any) { + if (isBucketMissingError(e)) { + return; + } + throw e; + } + } +} + +export function isStackMissingError(e: Error) { + return e.message.indexOf('does not exist') > -1; +} + +export function isBucketMissingError(e: Error) { + return e.message.indexOf('does not exist') > -1; +} + +/** + * Retry an async operation until a deadline is hit. + * + * Use `retry.forSeconds()` to construct a deadline relative to right now. + * + * Exceptions will cause the operation to retry. Use `retry.abort` to annotate an exception + * to stop the retry and end in a failure. + */ +export async function retry( + output: NodeJS.WritableStream, + operation: string, + deadline: Date, + block: () => Promise, +): Promise { + let i = 0; + output.write(`💈 ${operation}\n`); + while (true) { + try { + i++; + const ret = await block(); + output.write(`💈 ${operation}: succeeded after ${i} attempts\n`); + return ret; + } catch (e: any) { + if (e.abort || Date.now() > deadline.getTime()) { + throw new Error(`${operation}: did not succeed after ${i} attempts: ${e}`); + } + output.write(`⏳ ${operation} (${e.message})\n`); + await sleep(5000); + } + } +} + +/** + * Make a deadline for the `retry` function relative to the current time. + */ +retry.forSeconds = (seconds: number): Date => { + return new Date(Date.now() + seconds * 1000); +}; + +/** + * Annotate an error to stop the retrying + */ +retry.abort = (e: Error): Error => { + (e as any).abort = true; + return e; +}; + +export function outputFromStack(key: string, stack: Stack): string | undefined { + return (stack.Outputs ?? []).find((o) => o.OutputKey === key)?.OutputValue; +} + +export async function sleep(ms: number) { + return new Promise((ok) => setTimeout(ok, ms)); +} + +function chainableCredentials(region: string): AwsCredentialIdentityProvider { + if ((process.env.CODEBUILD_BUILD_ARN || process.env.GITHUB_RUN_ID) && process.env.AWS_PROFILE) { + // in codebuild we must assume the role that the cdk uses + // otherwise credentials will just be picked up by the normal sdk + // heuristics and expire after an hour. + return fromIni({ + clientConfig: { region }, + }); + } + + // Otherwise just get what's default + return fromNodeProviderChain({ clientConfig: { region } }); +} + +function isAwsCredentialIdentity(x: any): x is AwsCredentialIdentity { + return Boolean(x && typeof x === 'object' && x.accessKeyId); +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/cli/query-github.ts b/packages/@aws-cdk-testing/cli-integ/lib/cli/query-github.ts new file mode 100644 index 000000000..8a8d61309 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/cli/query-github.ts @@ -0,0 +1,56 @@ +import * as yargs from 'yargs'; +import { fetchPreviousVersion } from '../github'; + +async function main() { + const args = await yargs + .option('token', { + descripton: 'GitHub token (default: from environment GITHUB_TOKEN)', + alias: 't', + type: 'string', + requiresArg: true, + }) + .command('last-release', 'Query the last release', cmd => cmd + .option('prior-to', { + description: 'Return the most recent release before the given version', + alias: 'p', + type: 'string', + requiresArg: true, + }) + .option('major', { + description: 'Return the most recent release that matches', + alias: 'm', + type: 'string', + requiresArg: true, + })) + .demandCommand() + .help() + .showHelpOnFail(false) + .argv; + + const command = args._[0]; + + const token = args.token ?? process.env.GITHUB_TOKEN; + if (!token) { + throw new Error('Either pass --token or set GITHUB_TOKEN.'); + } + + switch (command) { + case 'last-release': + if (args['prior-to'] && args.major) { + throw new Error('Cannot pass both `--prior-to and --major at the same time'); + } + + // eslint-disable-next-line no-console + console.log(await fetchPreviousVersion(token, { + priorTo: args['prior-to'], + majorVersion: args.major, + })); + break; + } +} + +main().catch(e => { + // eslint-disable-next-line no-console + console.error(e); + process.exitCode = 1; +}); diff --git a/packages/@aws-cdk-testing/cli-integ/lib/cli/run-suite.ts b/packages/@aws-cdk-testing/cli-integ/lib/cli/run-suite.ts new file mode 100644 index 000000000..a825a599e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/cli/run-suite.ts @@ -0,0 +1,153 @@ +/* eslint-disable no-console */ +import * as path from 'path'; +import * as jest from 'jest'; +import * as yargs from 'yargs'; +import { ReleasePackageSourceSetup } from '../package-sources/release-source'; +import { RepoPackageSourceSetup, autoFindRoot } from '../package-sources/repo-source'; +import type { IPackageSourceSetup } from '../package-sources/source'; +import { serializeForSubprocess } from '../package-sources/subprocess'; + +async function main() { + const args = await yargs + .usage('$0 ') + .positional('SUITENAME', { + descripton: 'Name of the test suite to run', + type: 'string', + demandOption: true, + }) + .option('test', { + descripton: 'Test pattern to selectively run tests', + alias: 't', + type: 'string', + requiresArg: true, + }) + .option('test-file', { + description: 'The specific test file to run', + type: 'string', + requiresArg: true, + }) + .option('use-source', { + descripton: 'Use TypeScript packages from the given source repository (or "auto")', + alias: 's', + type: 'string', + requiresArg: true, + }) + .option('use-cli-release', { + descripton: 'Run the current tests against the CLI at the given version', + alias: 'u', + type: 'string', + requiresArg: true, + }) + .option('auto-source', { + alias: 'a', + description: 'Automatically find the source tree from the current working directory', + type: 'boolean', + requiresArg: false, + }) + .option('runInBand', { + descripton: 'Run all tests in one Node process', + alias: 'i', + type: 'boolean', + }) + .options('framework-version', { + description: 'Framework version to use, if different than the CLI version (not all suites respect this)', + alias: 'f', + type: 'string', + }) + .options('verbose', { + alias: 'v', + description: 'Run in verbose mode', + type: 'boolean', + requiresArg: false, + }) + .options('passWithNoTests', { + description: 'Allow passing if the test suite is not found (default true when IS_CANARY mode, false otherwise)', + type: 'boolean', + requiresArg: false, + }) + .options('maxWorkers', { + alias: 'w', + description: 'Specifies the maximum number of workers the worker-pool will spawn for running tests. We use a sensible default for running cli integ tests.', + type: 'string', + requiresArg: true, + }) + .help() + .showHelpOnFail(false) + .argv; + + const suiteName = args._[0] as string; + if (!suiteName) { + throw new Error('Usage: run-suite '); + } + + let packageSource: undefined | IPackageSourceSetup; + function usePackageSource(s: IPackageSourceSetup) { + if (packageSource) { + throw new Error('Cannot specify two package sources'); + } + packageSource = s; + } + + if (args['use-source'] || args['auto-source']) { + if (args['framework-version']) { + throw new Error('Cannot use --framework-version with --use-source'); + } + + const root = args['use-source'] && args['use-source'] !== 'auto' + ? args['use-source'] + : await autoFindRoot(); + + usePackageSource(new RepoPackageSourceSetup(root)); + } else if (args['use-cli-release']) { + usePackageSource(new ReleasePackageSourceSetup(args['use-cli-release'], args['framework-version'])); + } + if (!packageSource) { + throw new Error('Specify either --use-source or --use-cli-release'); + } + + console.log(`Package source: ${packageSource.description}`); + console.log(`Test suite: ${suiteName}`); + console.log(`Test version: ${thisPackageVersion()}`); + + await packageSource.prepare(); + serializeForSubprocess(packageSource); + + if (args.verbose) { + process.env.VERBOSE = '1'; + } + + // Motivation behind this behavior: when adding a new test suite to the pipeline, because of the way our + // Pipeline package works, the suite would be added to the pipeline AND as a canary immediately. The canary + // would fail until the package was actually released, so for canaries we make an exception so that the initial + // canary would succeed even if the suite wasn't yet available. The fact that the suite is not optional in + // the pipeline protects us from typos. + const passWithNoTests = args.passWithNoTests ?? !!process.env.IS_CANARY; + + // Communicate with the config file (integ.jest.config.js) + process.env.TEST_SUITE_NAME = suiteName; + + try { + await jest.run([ + '--randomize', + ...args.runInBand ? ['-i'] : [], + ...args.test ? ['-t', args.test] : [], + ...args.verbose ? ['--verbose'] : [], + ...args.maxWorkers ? [`--maxWorkers=${args.maxWorkers}`] : [], + ...passWithNoTests ? ['--passWithNoTests'] : [], + ...args['test-file'] ? [args['test-file']] : [], + ], path.resolve(__dirname, '..', '..', 'resources', 'integ.jest.config.js')); + } finally { + await packageSource.cleanup(); + } +} + +function thisPackageVersion(): string { + // eslint-disable-next-line @typescript-eslint/no-require-imports + return require('../../package.json').version; +} + +main().catch(e => { + // eslint-disable-next-line no-console + console.error(e); + process.exitCode = 1; +}); diff --git a/packages/@aws-cdk-testing/cli-integ/lib/cli/stage-distribution.ts b/packages/@aws-cdk-testing/cli-integ/lib/cli/stage-distribution.ts new file mode 100644 index 000000000..d7c371b88 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/cli/stage-distribution.ts @@ -0,0 +1,264 @@ +/* eslint-disable no-console */ +import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as glob from 'glob'; +import * as yargs from 'yargs'; +import { shell } from '..'; +import { TestRepository } from '../staging/codeartifact'; +import { uploadJavaPackages, mavenLogin } from '../staging/maven'; +import { uploadNpmPackages, npmLogin } from '../staging/npm'; +import { uploadDotnetPackages, nugetLogin } from '../staging/nuget'; +import { uploadPythonPackages, pypiLogin } from '../staging/pypi'; +import { UsageDir } from '../staging/usage-dir'; + +async function main() { + await yargs + .usage('$0 ') + .option('npm', { + description: 'Upload NPM packages only', + type: 'boolean', + requiresArg: false, + }) + .option('python', { + description: 'Upload Python packages only', + type: 'boolean', + requiresArg: false, + }) + .option('java', { + description: 'Upload Java packages only', + type: 'boolean', + requiresArg: false, + }) + .option('dotnet', { + description: 'Upload Dotnet packages only', + type: 'boolean', + requiresArg: false, + }) + .option('regression', { + description: 'Enable access to previous versions of the staged packages (this is expensive for CodeArtifact so we only do it when necessary)', + type: 'boolean', + requiresArg: false, + default: false, + }) + .command('publish ', 'Publish a given directory', cmd => cmd + .positional('DIRECTORY', { + descripton: 'Directory distribution', + type: 'string', + demandOption: true, + }) + .option('name', { + alias: 'n', + description: 'Name of the repository to create (default: generate unique name)', + type: 'string', + requiresArg: true, + }), async (args) => { + await validateDirectory(args); + const repo = await (args.name ? TestRepository.newWithName(args.name) : TestRepository.newRandom()); + const usageDir = UsageDir.default(); + + await doLogin(repo, usageDir, args); + await publish(repo, usageDir, args); + + header('Done'); + usageDir.advertise(); + }) + .command('login', 'Login to a given repository', cmd => cmd + .option('name', { + alias: 'n', + description: 'Name of the repository to log in to', + type: 'string', + requiresArg: true, + demandOption: true, + }), async (args) => { + const repo = TestRepository.existing(args.name); + const usageDir = UsageDir.default(); + + await doLogin(repo, usageDir, args); + + usageDir.advertise(); + }) + .command('run ', 'Publish and run a command', cmd => cmd + .positional('DIRECTORY', { + descripton: 'Directory distribution', + type: 'string', + demandOption: true, + }) + .positional('COMMAND', { + alias: 'c', + description: 'Run the given command with the packages staged', + type: 'string', + array: true, + demandOption: true, + }) + .option('cleanup', { + alias: 'C', + description: 'Cleanup the repository afterwards', + type: 'boolean', + default: true, + requiresArg: false, + }), async (args) => { + await validateDirectory(args); + const repo = await TestRepository.newRandom(); + const usageDir = UsageDir.default(); + + await doLogin(repo, usageDir, args); + await publish(repo, usageDir, args); + + try { + await usageDir.activateInCurrentProcess(); + + await shell(args.COMMAND ?? [], { + shell: true, + show: 'always', + }); + } finally { + if (args.cleanup) { + await repo.delete(); + } + } + }) + .command('cleanup', 'Clean up testing repository', cmd => cmd + .option('name', { + alias: 'n', + description: 'Name of the repository to cleanup (default: most recent)', + type: 'string', + requiresArg: true, + }), async (args) => { + const usageDir = UsageDir.default(); + + let repositoryName = args.name; + if (!repositoryName) { + repositoryName = (await usageDir.currentEnv()).CODEARTIFACT_REPO; + } + + if (!repositoryName) { + console.log(`No --name given and no $CODEARTIFACT_REPO found in ${usageDir.directory}, nothing cleaned up`); + return; + } + + const repo = TestRepository.existing(repositoryName); + await repo.delete(); + }) + .command('gc', 'Clean up day-old testing repositories', cmd => cmd, async () => { + await TestRepository.gc(); + }) + .demandCommand(1, 'You must supply a command') + .help() + .showHelpOnFail(false) + .parse(); +} + +async function validateDirectory(args: { + DIRECTORY: string; +}) { + if (!await fs.pathExists(path.join(args.DIRECTORY, 'build.json'))) { + throw new Error(`${args.DIRECTORY} does not look like a CDK dist directory (build.json missing)`); + } +} + +async function doLogin(repo: TestRepository, usageDir: UsageDir, args: { + npm?: boolean; + python?: boolean; + java?: boolean; + dotnet?: boolean; +}) { + const login = await repo.loginInformation(); + + const oldEnv = await usageDir.currentEnv(); + + await usageDir.clean(); + await usageDir.addToEnv({ + CODEARTIFACT_REPO: login.repositoryName, + }); + + if (oldEnv.BUILD_VERSION) { + await usageDir.addToEnv({ + BUILD_VERSION: oldEnv.BUILD_VERSION, + }); + } + + const doRepo = whichRepos(args); + + await doRepo.npm(() => npmLogin(login, usageDir)); + await doRepo.python(() => pypiLogin(login, usageDir)); + await doRepo.java(() => mavenLogin(login, usageDir)); + await doRepo.dotnet(() => nugetLogin(login, usageDir)); +} + +async function publish(repo: TestRepository, usageDir: UsageDir, args: { + DIRECTORY: string; + npm?: boolean; + python?: boolean; + java?: boolean; + dotnet?: boolean; + regression?: boolean; +}) { + const directory = `${args.DIRECTORY}`; + const login = await repo.loginInformation(); + + const doRepo = whichRepos(args); + + const buildJson = await fs.readJson(path.join(directory, 'build.json')); + await usageDir.addToEnv({ + BUILD_VERSION: buildJson.version, + }); + + await doRepo.npm(async () => { + header('NPM'); + await uploadNpmPackages(glob.sync(path.join(directory, 'js', '*.tgz')), login, usageDir); + }); + + await doRepo.python(async () => { + header('Python'); + await uploadPythonPackages(glob.sync(path.join(directory, 'python', '*')), login); + }); + + await doRepo.java(async () => { + header('Java'); + await uploadJavaPackages(glob.sync(path.join(directory, 'java', '**', '*.pom')), login, usageDir); + }); + + await doRepo.dotnet(async () => { + header('.NET'); + await uploadDotnetPackages(glob.sync(path.join(directory, 'dotnet', '**', '*.nupkg')), usageDir); + }); + + if (args.regression) { + console.log('🛍 Configuring packages for upstream versions'); + await repo.markAllUpstreamAllow(); + } +} + +function whichRepos(args: { + npm?: boolean; + python?: boolean; + java?: boolean; + dotnet?: boolean; +}) { + const all = args.npm === undefined && args.python === undefined && args.java === undefined && args.dotnet === undefined; + + const invoke = (block: () => Promise) => block(); + const skip = () => { + + }; + + return { + npm: args.npm || all ? invoke : skip, + python: args.python || all ? invoke : skip, + java: args.java || all ? invoke : skip, + dotnet: args.dotnet || all ? invoke : skip, + }; +} + +function header(caption: string) { + console.log(''); + console.log('/'.repeat(70)); + console.log(`// ${caption}`); + console.log(''); +} + +main().catch(e => { + // eslint-disable-next-line no-console + console.error(e); + process.exitCode = 1; +}); diff --git a/packages/@aws-cdk-testing/cli-integ/lib/cli/test-root.ts b/packages/@aws-cdk-testing/cli-integ/lib/cli/test-root.ts new file mode 100644 index 000000000..22bd437e6 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/cli/test-root.ts @@ -0,0 +1,3 @@ +import * as path from 'path'; +// eslint-disable-next-line no-console +console.log(path.resolve(__dirname, '..', '..')); diff --git a/packages/@aws-cdk-testing/cli-integ/lib/corking.ts b/packages/@aws-cdk-testing/cli-integ/lib/corking.ts new file mode 100644 index 000000000..689c1c6ba --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/corking.ts @@ -0,0 +1,33 @@ +/** + * Routines for corking stdout and stderr + */ +import * as stream from 'stream'; + +export class MemoryStream extends stream.Writable { + private parts = new Array(); + + public _write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void { + this.parts.push(chunk); + callback(); + } + + public buffer() { + return Buffer.concat(this.parts); + } + + public clear() { + this.parts.splice(0, this.parts.length); + } + + public async flushTo(strm: NodeJS.WritableStream): Promise { + const flushed = strm.write(this.buffer()); + if (!flushed) { + return new Promise(ok => strm.once('drain', ok)); + } + return; + } + + public toString() { + return this.buffer().toString(); + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/eventually.ts b/packages/@aws-cdk-testing/cli-integ/lib/eventually.ts new file mode 100644 index 000000000..b5dbc8333 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/eventually.ts @@ -0,0 +1,42 @@ +/** + * @param maxAttempts the maximum number of attempts + * @param interval interval in milliseconds to observe between attempts + */ +export type EventuallyOptions = { + maxAttempts?: number; + interval?: number; +}; + +const wait = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); +const DEFAULT_INTERVAL = 1000; +const DEFAULT_MAX_ATTEMPTS = 10; + +/** + * Runs a function on an interval until the maximum number of attempts has + * been reached. + * + * Default interval = 1000 milliseconds + * Default maxAttempts = 10 + * + * @param fn function to run + * @param options EventuallyOptions + */ +const eventually = async (call: () => Promise, options?: EventuallyOptions): Promise => { + const opts = { + interval: options?.interval ? options.interval : DEFAULT_INTERVAL, + maxAttempts: options?.maxAttempts ? options.maxAttempts : DEFAULT_MAX_ATTEMPTS, + }; + + while (opts.maxAttempts-- >= 0) { + try { + return await call(); + } catch (err) { + if (opts.maxAttempts <= 0) throw err; + } + await wait(opts.interval); + } + + throw new Error('An unexpected error has occurred.'); +}; + +export default eventually; diff --git a/packages/@aws-cdk-testing/cli-integ/lib/files.ts b/packages/@aws-cdk-testing/cli-integ/lib/files.ts new file mode 100644 index 000000000..47fb538d5 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/files.ts @@ -0,0 +1,80 @@ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs-extra'; + +export async function rmFile(filename: string) { + if (await fs.pathExists(filename)) { + await fs.unlink(filename); + } +} + +export async function addToFile(filename: string, line: string) { + let contents = await fs.pathExists(filename) ? await fs.readFile(filename, { encoding: 'utf-8' }) : ''; + if (!contents.endsWith('\n')) { + contents += '\n'; + } + contents += line + '\n'; + + await writeFile(filename, contents); +} + +export async function writeFile(filename: string, contents: string) { + await fs.mkdirp(path.dirname(filename)); + await fs.writeFile(filename, contents, { encoding: 'utf-8' }); +} + +export async function copyDirectoryContents(dir: string, target: string) { + for (const file of await fs.readdir(path.join(dir), { encoding: 'utf-8' })) { + await fs.copyFile(path.join(dir, file), path.join(target, file)); + } +} + +export function findUp(name: string, directory: string = process.cwd()): string | undefined { + const absoluteDirectory = path.resolve(directory); + + const file = path.join(directory, name); + if (fs.existsSync(file)) { + return file; + } + + const { root } = path.parse(absoluteDirectory); + if (absoluteDirectory == root) { + return undefined; + } + + return findUp(name, path.dirname(absoluteDirectory)); +} + +/** + * Docker-safe home directory + */ +export function homeDir() { + return os.userInfo().homedir ?? os.homedir(); +} + +export async function loadLines(filename: string): Promise { + return await fs.pathExists(filename) ? (await fs.readFile(filename, { encoding: 'utf-8' })).trim().split('\n') : []; +} + +export async function writeLines(filename: string, lines: string[]) { + // Must end in a newline or our bash script won't read it properly + await fs.writeFile(filename, lines.join('\n') + '\n', { encoding: 'utf-8' }); +} + +/** + * Update a spaceless ini file in place + */ +export function updateIniKey(lines: string[], key: string, value: string) { + const prefix = `${key}=`; + let found = false; + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith(prefix)) { + lines[i] = prefix + value; + found = true; + break; + } + } + if (!found) { + lines.push(prefix + value); + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/github.ts b/packages/@aws-cdk-testing/cli-integ/lib/github.ts new file mode 100644 index 000000000..f4dc3ae5f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/github.ts @@ -0,0 +1,47 @@ +import { Octokit } from '@octokit/rest'; +import * as semver from 'semver'; + +export async function fetchPreviousVersion(token: string, options?: { + priorTo?: string; + majorVersion?: string; +}) { + const github = new Octokit({ auth: token }); + const releases = await github.repos.listReleases({ + owner: 'aws', + repo: 'aws-cdk', + }); + + // this returns a list in descending order, newest releases first + // opts for same major version where possible, falling back otherwise + // to previous major versions. + let previousMVRelease = undefined; + for (const release of releases.data) { + const version = release.name?.replace('v', ''); + if (!version) { + continue; + } + + // Any old version is fine + if (!options?.majorVersion && !options?.priorTo) { + return version; + } + + if (options?.majorVersion && `${semver.major(version)}` === options.majorVersion) { + return version; + } + + if (options?.priorTo && semver.lt(version, options?.priorTo) && semver.major(version) === semver.major(options.priorTo)) { + return version; + } + + // Otherwise return the most recent version that didn't match any + if (!previousMVRelease) { + previousMVRelease = version; + } + } + if (previousMVRelease) { + return previousMVRelease; + } + + throw new Error(`Unable to find previous version given ${JSON.stringify(options)}`); +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/index.ts b/packages/@aws-cdk-testing/cli-integ/lib/index.ts new file mode 100644 index 000000000..83ebb0337 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/index.ts @@ -0,0 +1,13 @@ +export * from './aws'; +export * from './corking'; +export * from './integ-test'; +export * from './memoize'; +export * from './resource-pool'; +export * from './with-cli-lib'; +export * from './with-sam'; +export * from './shell'; +export * from './with-aws'; +export * from './with-cdk-app'; +export * from './with-packages'; +export * from './with-temporary-directory'; +export * from './resources'; diff --git a/packages/@aws-cdk-testing/cli-integ/lib/integ-test.ts b/packages/@aws-cdk-testing/cli-integ/lib/integ-test.ts new file mode 100644 index 000000000..88c853324 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/integ-test.ts @@ -0,0 +1,110 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { MemoryStream } from './corking'; + +const SKIP_TESTS = fs.readFileSync(path.join(__dirname, '..', 'skip-tests.txt'), { encoding: 'utf-8' }) + .split('\n') + .map(x => x.trim()) + .filter(x => x && !x.startsWith('#')); + +if (SKIP_TESTS.length > 0) { + process.stderr.write(`ℹ️ Skipping tests: ${JSON.stringify(SKIP_TESTS)}\n`); +} + +// Whether we want to stop after the first failure, for quicker debugging (hopefully). +const FAIL_FAST = process.env.FAIL_FAST === 'true'; + +// Keep track of whether the suite has failed. If so, we stop running. +let failed = false; + +export interface TestContext { + readonly randomString: string; + readonly name: string; + readonly output: NodeJS.WritableStream; + log(s: string): void; +} + +/** + * A wrapper for jest's 'test' which takes regression-disabled tests into account and prints a banner + */ +export function integTest( + name: string, + callback: (context: TestContext) => Promise, + timeoutMillis?: number, +): void { + const runner = shouldSkip(name) ? test.skip : test; + + runner(name, async () => { + const output = new MemoryStream(); + + output.write('================================================================\n'); + output.write(`${name}\n`); + output.write('================================================================\n'); + + const now = Date.now(); + process.stderr.write(`[INTEG TEST::${name}] Starting (pid ${process.pid})...\n`); + try { + if (FAIL_FAST && failed) { + throw new Error('FAIL_FAST requested and currently failing. Stopping test early.'); + } + + return await callback({ + output, + randomString: randomString(), + name, + log(s: string) { + output.write(`${s}\n`); + }, + }); + } catch (e: any) { + failed = true; + + // Print the buffered output, only if the test fails. + output.write(e.message); + output.write(e.stack); + process.stderr.write(`[INTEG TEST::${name}] Failed: ${e}\n`); + + const isGitHub = !!process.env.GITHUB_RUN_ID; + + if (isGitHub) { + // GitHub Actions compatible output formatting + // https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/workflow-commands-for-github-actions#setting-an-error-message + let written = process.stderr.write(`::error title=Failed ${name}::${e.message}\n`); + if (!written) { + // Wait for drain + await new Promise((ok) => process.stderr.once('drain', ok)); + } + + // Print output only if the test fails. Use 'console.log' so the output is buffered by + // jest and prints without a stack trace (if verbose: false). + written = process.stdout.write([ + `::group::Failure details: ${name} (click to expand)\n`, + `${output.buffer().toString()}\n`, + '::endgroup::\n', + ].join('')); + if (!written) { + // Wait for drain + await new Promise((ok) => process.stdout.once('drain', ok)); + } + } else { + // Use 'console.log' so the output is buffered by + // jest and prints without a stack trace (if verbose: false). + // eslint-disable-next-line no-console + console.log(output.buffer().toString()); + } + throw e; + } finally { + const duration = Date.now() - now; + process.stderr.write(`[INTEG TEST::${name}] Done (${duration} ms).\n`); + } + }, timeoutMillis); +} + +function shouldSkip(testName: string) { + return SKIP_TESTS.includes(testName); +} + +export function randomString() { + // Crazy + return Math.random().toString(36).replace(/[^a-z0-9]+/g, ''); +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/lists.ts b/packages/@aws-cdk-testing/cli-integ/lib/lists.ts new file mode 100644 index 000000000..043a55571 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/lists.ts @@ -0,0 +1,9 @@ +export function chunk(n: number, xs: A[]): A[][] { + const ret = new Array(); + + for (let i = 0; i < xs.length; i += n) { + ret.push(xs.slice(i, i + n)); + } + + return ret; +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/memoize.ts b/packages/@aws-cdk-testing/cli-integ/lib/memoize.ts new file mode 100644 index 000000000..57eb79f07 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/memoize.ts @@ -0,0 +1,14 @@ +/** + * Return a memoized version of an function with 0 arguments. + * + * Async-safe. + */ +export function memoize0(fn: () => Promise): () => Promise { + let promise: Promise | undefined; + return () => { + if (!promise) { + promise = fn(); + } + return promise; + }; +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/npm.ts b/packages/@aws-cdk-testing/cli-integ/lib/npm.ts new file mode 100644 index 000000000..fd7923c2e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/npm.ts @@ -0,0 +1,41 @@ +import { spawnSync } from 'child_process'; + +const MINIMUM_VERSION = '3.9'; + +/** + * Use NPM preinstalled on the machine to look up a list of TypeScript versions + */ +export function typescriptVersionsSync(): string[] { + const { stdout } = spawnSync('npm', ['--silent', 'view', `typescript@>=${MINIMUM_VERSION}`, 'version', '--json'], { encoding: 'utf-8' }); + + const versions: string[] = JSON.parse(stdout); + return Array.from(new Set(versions.map(v => v.split('.').slice(0, 2).join('.')))); +} + +/** + * Use NPM preinstalled on the machine to query publish times of versions + */ +export function typescriptVersionsYoungerThanDaysSync(days: number, versions: string[]): string[] { + const { stdout } = spawnSync('npm', ['--silent', 'view', 'typescript', 'time', '--json'], { encoding: 'utf-8' }); + const versionTsMap: Record = JSON.parse(stdout); + + const cutoffDate = new Date(Date.now() - (days * 24 * 3600 * 1000)); + const cutoffDateS = cutoffDate.toISOString(); + + const recentVersions = Object.entries(versionTsMap) + .filter(([_, dateS]) => dateS > cutoffDateS) + .map(([v]) => v); + + // Input versions are of the form 3.9, 5.2, etc. + // Actual versions are of the form `3.9.15`, `5.3.0-dev.20511311`. + // Return only 2-digit versions for which there is a non-prerelease version in the set of recentVersions + // So a 2-digit versions that is followed by `.` until the end of the string. + return versions.filter((twoV) => { + const re = new RegExp(`^${reQuote(twoV)}\\.\\d+$`); + return recentVersions.some(fullV => fullV.match(re)); + }); +} + +function reQuote(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/release-source.ts b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/release-source.ts new file mode 100644 index 000000000..a207cdd39 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/release-source.ts @@ -0,0 +1,82 @@ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import type { IPackageSourceSetup, IPackageSource } from './source'; +import { copyDirectoryContents } from '../files'; +import { shell, rimraf, addToShellPath } from '../shell'; + +export class ReleasePackageSourceSetup implements IPackageSourceSetup { + readonly name = 'release'; + readonly description: string; + + private tempDir?: string; + + constructor(private readonly version: string, private readonly frameworkVersion?: string) { + this.description = `release @ ${this.version}`; + } + + public async prepare(): Promise { + this.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tmpcdk')); + fs.mkdirSync(this.tempDir, { recursive: true }); + + await shell(['node', require.resolve('npm'), 'install', `aws-cdk@${this.version}`], { + cwd: this.tempDir, + }); + + process.env.CDK_CLI_PATH = this.tempDir; + process.env.VERSION = this.version; + process.env.FRAMEWORK_VERSION = this.frameworkVersion ?? this.version; + } + + public async cleanup(): Promise { + if (this.tempDir) { + rimraf(this.tempDir); + } + } +} + +export class ReleasePackageSource implements IPackageSource { + private readonly cliPath: string; + private readonly version: string; + + constructor() { + this.cliPath = process.env.CDK_CLI_PATH!; + this.version = process.env.VERSION!; + } + + public async makeCliAvailable() { + addToShellPath(path.join(this.cliPath, 'node_modules', '.bin')); + } + + public assertJsiiPackagesAvailable() { + } + + public async initializeDotnetPackages(currentDir: string) { + if (process.env.CWD_FILES_DIR) { + await copyDirectoryContents(process.env.CWD_FILES_DIR, currentDir); + } + } + + public majorVersion() { + return this.version.split('.')[0] as string; + } + + public requestedCliVersion() { + return this.version; + } + + public requestedFrameworkVersion() { + return process.env.FRAMEWORK_VERSION!; + } + + public requestedAlphaVersion(): string { + const frameworkVersion = this.requestedFrameworkVersion(); + if (frameworkVersion.includes('-rc.')) { + // For a pipeline release + return frameworkVersion.replace(/-rc\.\d+$/, '-alpha.999'); + } else { + // For a stable release + return `${frameworkVersion}-alpha.0`; + } + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts new file mode 100644 index 000000000..0314216b9 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-source.ts @@ -0,0 +1,112 @@ +import * as os from 'os'; +import * as path from 'path'; +import * as fs from 'fs-extra'; +import type { IPackageSourceSetup, IPackageSource } from './source'; +import { findUp } from '../files'; +import { shell, addToShellPath } from '../shell'; + +export class RepoPackageSourceSetup implements IPackageSourceSetup { + readonly name = 'repo'; + readonly description: string; + + constructor(private readonly repoRoot: string) { + this.description = `repo(${this.repoRoot})`; + } + + public async prepare(): Promise { + if (!await fs.pathExists(path.join(this.repoRoot, 'package.json')) || !await fs.pathExists(path.join(this.repoRoot, 'yarn.lock'))) { + throw new Error(`${this.repoRoot}: does not look like the repository root`); + } + + process.env.REPO_ROOT = this.repoRoot; + process.env.REPO_PACKAGE_MAP = await writePackageMap(this.repoRoot); + addToShellPath(path.resolve(__dirname, 'repo-tools')); + } + + public async cleanup(): Promise { + } +} + +export class RepoPackageSource implements IPackageSource { + private readonly repoRoot: string; + + constructor() { + this.repoRoot = process.env.REPO_ROOT as string; + } + + public async makeCliAvailable() { + addToShellPath(path.join(this.repoRoot, 'packages', 'aws-cdk', 'bin')); + } + + public assertJsiiPackagesAvailable() { + throw new Error('jsii client packages are not available when using REPO source'); + } + + public async initializeDotnetPackages() { + } + + public majorVersion() { + const releaseJson = fs.readJsonSync(path.resolve(this.repoRoot, 'release.json')); + return releaseJson.majorVersion; + } + + public requestedCliVersion(): string { + return '*'; + } + + public requestedFrameworkVersion(): string { + return '*'; + } + + public requestedAlphaVersion(): string { + return '*'; + } +} + +async function writePackageMap(repoRoot: string): Promise { + const packages = await findYarnPackages(repoRoot); + const fileName = path.join(os.tmpdir(), 'package-map.json'); + await fs.writeJson(fileName, packages); + return fileName; +} + +/** + * Cache monorepo discovery results, we only want to do this once per run + */ +const YARN_MONOREPO_CACHE: Record = {}; + +/** + * Return a { name -> directory } packages found in a Yarn monorepo + * + * Cached in YARN_MONOREPO_CACHE. + */ +export async function findYarnPackages(root: string): Promise> { + if (!(root in YARN_MONOREPO_CACHE)) { + const outputDataString: string = JSON.parse(await shell(['yarn', 'workspaces', '--json', 'info'], { + captureStderr: false, + cwd: root, + show: 'error', + })).data; + const output: YarnWorkspacesOutput = JSON.parse(outputDataString); + + const ret: Record = {}; + for (const [k, v] of Object.entries(output)) { + ret[k] = path.join(root, v.location); + } + YARN_MONOREPO_CACHE[root] = ret; + } + return YARN_MONOREPO_CACHE[root]; +} + +/** + * Find the root directory of the repo from the current directory + */ +export async function autoFindRoot() { + const found = findUp('release.json'); + if (!found) { + throw new Error(`Could not determine repository root: 'release.json' not found from ${process.cwd()}`); + } + return path.dirname(found); +} + +type YarnWorkspacesOutput = Record; diff --git a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-tools/npm b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-tools/npm new file mode 100644 index 000000000..ab3229feb --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-tools/npm @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('./npm.js'); \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-tools/npm.ts b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-tools/npm.ts new file mode 100644 index 000000000..38e0e1e87 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/repo-tools/npm.ts @@ -0,0 +1,48 @@ +import * as child_process from 'child_process'; +import * as fs from 'fs-extra'; + +let argv = process.argv.slice(2); + +// eslint-disable-next-line no-console +console.log('fake npm'); + +if (argv[0] === 'install') { + if (!process.env.REPO_PACKAGE_MAP) { + throw new Error('REPO_PACKAGE_MAP not set'); + } + const repoPackageMap = fs.readJsonSync(process.env.REPO_PACKAGE_MAP, { encoding: 'utf-8' }); + + // Replace paths in the 'package.json' in the current directory + if (fs.pathExistsSync('package.json')) { + const packageJson = fs.readJsonSync('package.json', { encoding: 'utf-8' }); + for (const deps of [packageJson.dependencies ?? {}, packageJson.devDependencies ?? {}]) { + for (const [name, version] of Object.entries(deps)) { + deps[name] = repoPackageMap[name] ?? version; + } + } + fs.writeJsonSync('package.json', packageJson, { encoding: 'utf-8' }); + } + + // Replace package names on the command line + argv = argv.map(x => repoPackageMap[x] ?? x); +} + +//////////////////////////////////////////////////////////////////////// +// Shell out to original npm + +const child = child_process.spawn('node', [require.resolve('npm'), ...argv], { + shell: false, + stdio: ['ignore', 'inherit', 'inherit'], +}); + +child.once('error', e => { + // eslint-disable-next-line no-console + console.error(e); + process.exitCode = 1; +}); + +child.once('close', code => { + if (code) { + process.exitCode = code; + } +}); diff --git a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/source.ts b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/source.ts new file mode 100644 index 000000000..6edc726dd --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/source.ts @@ -0,0 +1,35 @@ +export interface IPackageSourceSetup { + readonly name: string; + readonly description: string; + + prepare(): Promise; + cleanup(): Promise; +} + +export interface IPackageSource { + makeCliAvailable(): Promise; + + assertJsiiPackagesAvailable(): void; + majorVersion(): string; + + initializeDotnetPackages(targetDir: string): Promise; + + /** + * CLI version + */ + requestedCliVersion(): string; + + /** + * Framework version if it's different than the CLI version + * + * Not all tests will respect this. + */ + requestedFrameworkVersion(): string; + + /** + * Versions of alpha packages if different than the CLI version + * + * Not all tests will respect this. + */ + requestedAlphaVersion(): string; +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/subprocess.ts b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/subprocess.ts new file mode 100644 index 000000000..31863248b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/subprocess.ts @@ -0,0 +1,15 @@ +import { ReleasePackageSource } from './release-source'; +import { RepoPackageSource } from './repo-source'; +import type { IPackageSourceSetup, IPackageSource } from './source'; + +export function serializeForSubprocess(s: IPackageSourceSetup) { + process.env.PACKAGE_SOURCE = s.name; +} + +export function packageSourceInSubprocess(): IPackageSource { + switch (process.env.PACKAGE_SOURCE) { + case 'repo': return new RepoPackageSource(); + case 'release': return new ReleasePackageSource(); + default: throw new Error(`Unrecognized package source: ${process.env.PACKAGE_SOURCE}`); + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/process.ts b/packages/@aws-cdk-testing/cli-integ/lib/process.ts new file mode 100644 index 000000000..6cf523e74 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/process.ts @@ -0,0 +1,147 @@ +import * as child from 'child_process'; +import type { Readable, Writable } from 'stream'; +import * as pty from 'node-pty'; + +/** + * IProcess provides an interface to work with a subprocess. + */ +export interface IProcess { + + /** + * Register a callback to be invoked when a chunk is written to stdout. + */ + onStdout(callback: (chunk: Buffer) => void): void; + + /** + * Register a callback to be invoked when a chunk is written to stderr. + */ + onStderr(callback: (chunk: Buffer) => void): void; + + /** + * Register a callback to be invoked when the process exists. + */ + onExit(callback: (exitCode: number) => void): void; + + /** + * Register a callback to be invoked if the process failed to start. + */ + onError(callback: (error: Error) => void): void; + + /** + * Write the process stdin stream. + */ + writeStdin(data: string): void; + + /** + * Singal that no more data will be written to stdin. In non tty process you must + * call this method to make sure the process exits. + * + * @param delay - optional delay in milliseconds before the signal is sent. + * + */ + endStdin(delay?: number): void; + +} + +export class Process { + /** + * Spawn a process with a TTY attached. + */ + public static spawnTTY(command: string, args: string[], options: pty.IPtyForkOptions | pty.IWindowsPtyForkOptions = {}): IProcess { + const process = pty.spawn(command, args, { + name: 'xterm-color', + ...options, + }); + return new PtyProcess(process); + } + + /** + * Spawn a process without a forcing a TTY. + */ + public static spawn(command: string, args: string[], options: child.SpawnOptions = {}): IProcess { + const process = child.spawn(command, args, { + shell: true, + stdio: ['ignore', 'pipe', 'pipe'], + ...options, + }); + return new NonPtyProcess(process); + } +} + +class PtyProcess implements IProcess { + public constructor(private readonly process: pty.IPty) { + } + + public endStdin(_?: number): void { + // not needed because all streams are the same in tty. + } + + public onError(_: (error: Error) => void): void { + // not needed because the pty.spawn will simply fail in this case. + } + + public onStdout(callback: (chunk: Buffer) => void): void { + this.process.onData((e) => callback(Buffer.from(e))); + } + + public onStderr(_callback: (chunk: Buffer) => void): void { + // https://github.com/microsoft/node-pty/issues/71 + throw new Error('Cannot register callback for \'stderr\'. A tty does not have separate output and error channels'); + } + + public onExit(callback: (exitCode: number) => void): void { + this.process.onExit((e) => { + callback(e.exitCode); + }); + } + + public writeStdin(data: string): void { + // in a pty all streams are the same + this.process.write(data); + } +} + +class NonPtyProcess implements IProcess { + public constructor(private readonly process: child.ChildProcess) { + } + + public onError(callback: (error: Error) => void): void { + this.process.once('error', callback); + } + + public onStdout(callback: (chunk: Buffer) => void): void { + this.assertDefined('stdout', this.process.stdout); + this.process.stdout.on('data', callback); + } + + public onStderr(callback: (chunk: Buffer) => void): void { + this.assertDefined('stderr', this.process.stderr); + this.process.stderr.on('data', callback); + } + + public onExit(callback: (exitCode: number) => void): void { + this.process.on('close', callback); + } + + public writeStdin(content: string): void { + this.assertDefined('stdin', this.process.stdin); + this.process.stdin.write(content); + } + + public endStdin(delay?: number): void { + if (this.process.stdin == null) { + throw new Error('No stdin defined for process'); + } + if (delay) { + setTimeout(() => this.process.stdin!.end(), delay); + } else { + this.process.stdin!.end(); + } + } + + public assertDefined(name: 'stdin' | 'stdout' | 'stderr', stream?: Readable | Writable | undefined | null): asserts stream { + if (stream == null) { + throw new Error(`No ${name} defined for child process`); + } + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/proxy.ts b/packages/@aws-cdk-testing/cli-integ/lib/proxy.ts new file mode 100644 index 000000000..2db74f111 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/proxy.ts @@ -0,0 +1,64 @@ +import { promises as fs } from 'fs'; +import * as querystring from 'node:querystring'; +import * as os from 'os'; +import * as path from 'path'; +import * as mockttp from 'mockttp'; +import type { CompletedRequest } from 'mockttp'; + +export async function startProxyServer(certDirRoot?: string): Promise { + const certDir = await fs.mkdtemp(path.join(certDirRoot ?? os.tmpdir(), 'cdk-')); + const certPath = path.join(certDir, 'cert.pem'); + const keyPath = path.join(certDir, 'key.pem'); + + // Set up key and certificate + const { key, cert } = await mockttp.generateCACertificate(); + await fs.writeFile(keyPath, key); + await fs.writeFile(certPath, cert); + + const server = mockttp.getLocal({ + https: { keyPath: keyPath, certPath: certPath }, + }); + + // We don't need to modify any request, so the proxy + // passes through all requests to the target host. + const endpoint = await server + .forAnyRequest() + .thenPassThrough(); + + const port = 9000 + Math.floor(Math.random() * 10000); + + // server.enableDebug(); + await server.start(port); + + return { + certPath, + keyPath, + server, + url: server.url, + port: server.port, + getSeenRequests: () => endpoint.getSeenRequests(), + async stop() { + await server.stop(); + await fs.rm(certDir, { recursive: true, force: true }); + }, + }; +} + +export interface ProxyServer { + readonly certPath: string; + readonly keyPath: string; + readonly server: mockttp.Mockttp; + readonly url: string; + readonly port: number; + + getSeenRequests(): Promise; + stop(): Promise; +} + +export function awsActionsFromRequests(requests: CompletedRequest[]): string[] { + return [...new Set(requests + .map(req => req.body.buffer.toString('utf-8')) + .map(body => querystring.decode(body)) + .map(x => x.Action as string) + .filter(action => action != null))]; +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/resource-pool.ts b/packages/@aws-cdk-testing/cli-integ/lib/resource-pool.ts new file mode 100644 index 000000000..45ab4c69b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/resource-pool.ts @@ -0,0 +1,143 @@ +import type { ILock, XpMutex } from './xpmutex'; +import { XpMutexPool } from './xpmutex'; + +/** + * A class that holds a pool of resources and gives them out and returns them on-demand + * + * The resources will be given out front to back, when they are returned + * the most recently returned version will be given out again (for best + * cache coherency). + * + * If there are multiple consumers waiting for a resource, consumers are serviced + * in FIFO order for most fairness. + */ +export class ResourcePool { + public static withResources(name: string, resources: A[]) { + const pool = XpMutexPool.fromName(name); + return new ResourcePool(pool, resources); + } + + private readonly resources: ReadonlyArray; + private readonly mutexes: Record = {}; + private readonly locks: Record = {}; + + private constructor(private readonly pool: XpMutexPool, resources: A[]) { + if (resources.length === 0) { + throw new Error('Must have at least one resource in the pool'); + } + + // Shuffle to reduce contention + resources = [...resources]; + fisherYatesShuffle(resources); + this.resources = resources; + + for (const res of resources) { + this.mutexes[res] = this.pool.mutex(res); + } + } + + /** + * Take one value from the resource pool + * + * If no such value is currently available, wait until it is. + */ + public async take(): Promise> { + while (true) { + // Start a wait on the unlock now -- if the unlock signal comes after + // we try to acquire but before we start the wait, we might miss it. + // + // (The timeout is in case the unlock signal doesn't come for whatever reason). + const wait = this.pool.awaitUnlock(10_000); + + // Try all mutexes, we might need to reacquire an expired lock + for (const res of this.resources) { + const lease = await this.tryObtainLease(res); + if (lease) { + // Ignore the wait (count as handled) + wait.then(() => { + }, () => { + }); + return lease; + } + } + + // None available, wait until one gets unlocked then try again + await wait; + } + } + + /** + * Execute a block using a single resource from the pool + */ + public async using(block: (x: A) => B | Promise): Promise { + const lease = await this.take(); + try { + return await block(lease.value); + } finally { + await lease.dispose(); + } + } + + private async tryObtainLease(value: A) { + const lock = await this.mutexes[value].tryAcquire(); + if (!lock) { + return undefined; + } + + this.locks[value] = lock; + return this.makeLease(value); + } + + private makeLease(value: A): ILease { + let disposed = false; + return { + value, + dispose: async () => { + if (disposed) { + throw new Error('Calling dispose() on an already-disposed lease.'); + } + disposed = true; + return this.returnValue(value); + }, + }; + } + + /** + * When a value is returned: + * + * - If someone's waiting for it, give it to them + * - Otherwise put it back into the pool + */ + private async returnValue(value: string) { + const lock = this.locks[value]; + delete this.locks[value]; + await lock?.release(); + } +} + +/** + * A single value taken from the pool + */ +export interface ILease { + /** + * The value obtained by the lease + */ + readonly value: A; + + /** + * Return the leased value to the pool + */ + dispose(): Promise; +} + +/** + * Shuffle an array in-place + */ +function fisherYatesShuffle(xs: A[]) { + for (let i = xs.length - 1; i >= 1; i--) { + const j = Math.floor(Math.random() * i); + const h = xs[j]; + xs[j] = xs[i]; + xs[i] = h; + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/resources.ts b/packages/@aws-cdk-testing/cli-integ/lib/resources.ts new file mode 100644 index 000000000..39ab24ab4 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/resources.ts @@ -0,0 +1,4 @@ +import * as path from 'path'; + +export const RESOURCES_DIR = path.resolve(__dirname, '..', 'resources'); + diff --git a/packages/@aws-cdk-testing/cli-integ/lib/shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts new file mode 100644 index 000000000..937e08fca --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/shell.ts @@ -0,0 +1,332 @@ +import type * as child_process from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import type { TestContext } from './integ-test'; +import { Process } from './process'; +import type { TemporaryDirectoryContext } from './with-temporary-directory'; + +/** + * A shell command that does what you want + * + * Is platform-aware, handles errors nicely. + */ +export async function shell(command: string[], options: ShellOptions = {}): Promise { + if (options.modEnv && options.env) { + throw new Error('Use either env or modEnv but not both'); + } + + const outputs = new Set(options.outputs); + const writeToOutputs = (x: string) => { + for (const outputStream of outputs) { + outputStream.write(x); + } + }; + + // Always output the command + writeToOutputs(`💻 ${command.join(' ')}\n`); + const show = options.show ?? 'always'; + + const env = options.env ?? (options.modEnv ? { ...process.env, ...options.modEnv } : process.env); + const tty = options.interact && options.interact.length > 0; + + // Coerce to `any` because `ShellOptions` contains custom properties + // that don't exist in the underlying interfaces. We could either rebuild each options map, + // or just pass through and let the underlying implemenation ignore what it doesn't know about. + // We choose the lazy one. + const spawnOptions = { ...options, env } as any; + + const child = tty + ? Process.spawnTTY(command[0], command.slice(1), spawnOptions) + : Process.spawn(command[0], command.slice(1), spawnOptions); + + // copy because we will be shifting it + const remainingInteractions = [...(options.interact ?? [])]; + + return new Promise((resolve, reject) => { + const stdout = new Array(); + const stderr = new Array(); + + const lastLine = new LastLine(); + + child.onStdout(chunk => { + if (show === 'always') { + writeToOutputs(chunk.toString('utf-8')); + } + stdout.push(chunk); + lastLine.append(chunk.toString('utf-8')); + + const interaction = remainingInteractions[0]; + if (interaction) { + if (interaction.prompt.test(lastLine.get())) { + // subprocess expects a user input now. + // first, shift the interactions to ensure the same interaction is not reused + remainingInteractions.shift(); + + // then, reset the last line to prevent repeated matches caused by tty echoing + lastLine.reset(); + + // now write the input with a slight delay to ensure + // the child process has already started reading. + setTimeout(() => { + child.writeStdin(interaction.input + (interaction.end ?? os.EOL)); + }, 500); + } + } + }); + + if (tty && options.captureStderr === false) { + // in a tty stderr goes to the same fd as stdout + throw new Error('Cannot disable \'captureStderr\' in tty'); + } + + if (!tty) { + // in a tty stderr goes to the same fd as stdout, so onStdout + // is sufficient. + child.onStderr(chunk => { + if (show === 'always') { + writeToOutputs(chunk.toString('utf-8')); + } + if (options.captureStderr ?? true) { + stderr.push(chunk); + } + }); + } + + child.onError(reject); + + child.onExit(code => { + const stderrOutput = Buffer.concat(stderr).toString('utf-8'); + const stdoutOutput = Buffer.concat(stdout).toString('utf-8'); + const out = (options.onlyStderr ? stderrOutput : stdoutOutput + stderrOutput).trim(); + + const logAndreject = (error: Error) => { + if (show === 'error') { + writeToOutputs(`${out}\n`); + } + reject(error); + }; + + if (remainingInteractions.length !== 0) { + // regardless of the exit code, if we didn't consume all expected interactions we probably + // did somethiing wrong. + logAndreject(new Error(`Expected more user interactions but subprocess exited with ${code}`)); + return; + } + + if (code === 0 || options.allowErrExit) { + resolve(out); + } else { + logAndreject(new Error(`'${command.join(' ')}' exited with error code ${code}.`)); + } + }); + }); +} + +/** + * Models a single user interaction with the shell. + */ +export interface UserInteraction { + /** + * The prompt to expect. Regex matched against the last line in + * the output before the prompt is displayed. + * + * Most commonly this would be a simple string to match for inclusion. + * + * Examples: + * + * - Process Output: "Hey there! Are you sure?" + * Prompt: /Are you sure?/ + * Match (Yes/No): Yes + * Reason: "Hey there! Are you sure?" ~ /Are you sure?/ + * + * - Process Output: "Hey there!\nAre you sure?" + * Prompt: /Are you sure?/ + * Match (Yes/No): Yes + * Reason: "Are you sure?" ~ /Are you sure?/ + * + * - Process Output: "Are you sure?\n(remember this is destructive)" + * Prompt: /Are you sure?/ + * Match (Yes/No): No + * Reason: "(remember this is destructive)" ≄ /Are you sure?/ + * + * - Process Output: "Are you sure?\n(remember this is destructive)" + * Prompt: /remember this is destructive/ + * Match (Yes/No): Yes + * Reason: "(remember this is destructive)" ~ /remember this is destructive/ + * + */ + readonly prompt: RegExp; + /** + * The input to provide. + */ + readonly input: string; + + /** + * The string to signal the end of input. + * + * @default os.EOL + */ + readonly end?: string; +} + +export interface ShellOptions extends child_process.SpawnOptions { + /** + * Properties to add to 'env' + */ + readonly modEnv?: Record; + + /** + * Don't fail when exiting with an error + * + * @default false + */ + readonly allowErrExit?: boolean; + + /** + * Whether to capture stderr + * + * @default true + */ + readonly captureStderr?: boolean; + + /** + * Pass output here + */ + readonly outputs?: NodeJS.WritableStream[]; + + /** + * Only return stderr. For example, this is used to validate + * that when CI=true, all logs are sent to stdout. + * + * @default false + */ + readonly onlyStderr?: boolean; + + /** + * Don't log to stdout + * + * @default always + */ + readonly show?: 'always' | 'never' | 'error'; + + /** + * Provide user interaction to respond to shell prompts. + * + * Order and count should correspond to the expected prompts issued by the subprocess. + */ + readonly interact?: UserInteraction[]; + +} + +export class ShellHelper { + public static fromContext(context: TestContext & TemporaryDirectoryContext) { + return new ShellHelper(context.integTestDir, context.output); + } + + constructor( + private readonly _cwd: string, + private readonly _output: NodeJS.WritableStream) { + + } + + public async shell(command: string[], options: Omit = {}): Promise { + return shell(command, { + outputs: [this._output], + cwd: this._cwd, + ...options, + modEnv: { + // give every shell its own docker config directory + // so that parallel runs don't interfere with each other. + DOCKER_CONFIG: path.join(this._cwd, '.docker'), + ...options.modEnv, + }, + }); + } +} + +/** + * rm -rf reimplementation, don't want to depend on an NPM package for this + * + * Returns `true` if everything got deleted, or `false` if some files could + * not be deleted due to permissions issues. + */ +export function rimraf(fsPath: string): boolean { + try { + let success = true; + const isDir = fs.lstatSync(fsPath).isDirectory(); + + if (isDir) { + for (const file of fs.readdirSync(fsPath)) { + success &&= rimraf(path.join(fsPath, file)); + } + fs.rmdirSync(fsPath); + } else { + fs.unlinkSync(fsPath); + } + return success; + } catch (e: any) { + // Can happen if some files got generated inside a Docker container and are now inadvertently owned by `root`. + // We can't ever clean those up anymore, but since it only happens inside GitHub Actions containers we also don't care too much. + if (e.code === 'EACCES' || e.code === 'ENOTEMPTY') { + return false; + } + + // Already gone + if (e.code === 'ENOENT') { + return true; + } + + throw e; + } +} + +export function addToShellPath(x: string) { + const parts = process.env.PATH?.split(':') ?? []; + + if (!parts.includes(x)) { + parts.unshift(x); + } + + process.env.PATH = parts.join(':'); +} + +/** + * Accumulate text since the last line break (or beginning of string) it has seen in the chunks. + * + * Examples: + * + * - Chunks: ['one\n', 'two\n', three'] + * - Last Line: 'three' + * + * - Chunks: ['one', 'two', '\nthree'] + * - Last Line: 'three' + * + * - Chunks: ['one', 'two'] + * - Last Line: 'onetwo' + * + * - Chunks: ['one', 'two', '\nthree', 'four'] + * - Last Line: 'threefour' + */ +class LastLine { + private lastLine: string = ''; + + public append(chunk: string): void { + const lines = chunk.split(os.EOL); + if (lines.length === 1) { + // chunk doesn't contain a new line so just append + this.lastLine += lines[0]; + } else { + // chunk contains multiple lines so just override with the last one + this.lastLine = lines[lines.length - 1]; + } + } + + public get(): string { + return this.lastLine; + } + + public reset() { + this.lastLine = ''; + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/staging/codeartifact.ts b/packages/@aws-cdk-testing/cli-integ/lib/staging/codeartifact.ts new file mode 100644 index 000000000..0c69707ce --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/staging/codeartifact.ts @@ -0,0 +1,388 @@ +import type { ListPackagesRequest } from '@aws-sdk/client-codeartifact'; +import { + AssociateExternalConnectionCommand, + CodeartifactClient, + CreateDomainCommand, + CreateRepositoryCommand, + DeleteRepositoryCommand, + DescribeDomainCommand, + DescribeRepositoryCommand, + GetAuthorizationTokenCommand, + GetRepositoryEndpointCommand, + ListPackagesCommand, + ListRepositoriesCommand, + ListTagsForResourceCommand, + PutPackageOriginConfigurationCommand, +} from '@aws-sdk/client-codeartifact'; +import { sleep } from '../aws'; + +const COLLECT_BY_TAG = 'collect-by'; +const REPO_LIFETIME_MS = 24 * 3600 * 1000; // One day + +export class TestRepository { + public static readonly DEFAULT_DOMAIN = 'test-cdk'; + + public static async newRandom() { + const qualifier = Math.random() + .toString(36) + .replace(/[^a-z0-9]+/g, ''); + + const repo = new TestRepository(`test-${qualifier}`); + await repo.prepare(); + return repo; + } + + public static async newWithName(name: string) { + const repo = new TestRepository(name); + await repo.prepare(); + return repo; + } + + public static existing(repositoryName: string) { + return new TestRepository(repositoryName); + } + + /** + * Garbage collect repositories + */ + public static async gc() { + if (!(await TestRepository.existing('*dummy*').domainExists())) { + return; + } + + const codeArtifact = new CodeartifactClient(); + + let nextToken: string | undefined; + do { + const page = await codeArtifact.send( + new ListRepositoriesCommand({ + nextToken: nextToken, + }), + ); + + for (const repo of page.repositories ?? []) { + const tags = await codeArtifact.send( + new ListTagsForResourceCommand({ + resourceArn: repo.arn!, + }), + ); + const collectable = tags?.tags?.find((t) => t.key === COLLECT_BY_TAG && Number(t.value) < Date.now()); + if (collectable) { + // eslint-disable-next-line no-console + console.log('Deleting', repo.name); + await codeArtifact.send( + new DeleteRepositoryCommand({ + domain: repo.domainName!, + repository: repo.name!, + }), + ); + } + } + + nextToken = page.nextToken; + } while (nextToken); + } + + public readonly npmUpstream = 'npm-upstream'; + public readonly pypiUpstream = 'pypi-upstream'; + public readonly nugetUpstream = 'nuget-upstream'; + public readonly mavenUpstream = 'maven-upstream'; + public readonly domain = TestRepository.DEFAULT_DOMAIN; + + private readonly codeArtifact = new CodeartifactClient(); + + private _loginInformation: LoginInformation | undefined; + + private constructor(public readonly repositoryName: string) { + } + + public async prepare() { + await this.ensureDomain(); + await this.ensureUpstreams(); + + await this.ensureRepository(this.repositoryName, { + description: 'Testing repository', + upstreams: [this.npmUpstream, this.pypiUpstream, this.nugetUpstream, this.mavenUpstream], + tags: { + [COLLECT_BY_TAG]: `${Date.now() + REPO_LIFETIME_MS}`, + }, + }); + } + + public async loginInformation(): Promise { + if (this._loginInformation) { + return this._loginInformation; + } + + this._loginInformation = { + authToken: ( + await this.codeArtifact.send( + new GetAuthorizationTokenCommand({ + domain: this.domain, + durationSeconds: 12 * 3600, + }), + ) + ).authorizationToken!, + repositoryName: this.repositoryName, + + npmEndpoint: ( + await this.codeArtifact.send( + new GetRepositoryEndpointCommand({ + domain: this.domain, + repository: this.repositoryName, + format: 'npm', + }), + ) + ).repositoryEndpoint!, + + mavenEndpoint: ( + await this.codeArtifact.send( + new GetRepositoryEndpointCommand({ + domain: this.domain, + repository: this.repositoryName, + format: 'maven', + }), + ) + ).repositoryEndpoint!, + + nugetEndpoint: ( + await this.codeArtifact.send( + new GetRepositoryEndpointCommand({ + domain: this.domain, + repository: this.repositoryName, + format: 'nuget', + }), + ) + ).repositoryEndpoint!, + + pypiEndpoint: ( + await this.codeArtifact.send( + new GetRepositoryEndpointCommand({ + domain: this.domain, + repository: this.repositoryName, + format: 'pypi', + }), + ) + ).repositoryEndpoint!, + }; + return this._loginInformation; + } + + public async delete() { + try { + await this.codeArtifact.send( + new DeleteRepositoryCommand({ + domain: this.domain, + repository: this.repositoryName, + }), + ); + + // eslint-disable-next-line no-console + console.log('Deleted', this.repositoryName); + } catch (e: any) { + if (e.name !== 'ResourceNotFoundException') { + throw e; + } + // Okay + } + } + + /** + * List all packages and mark them as "allow upstream versions". + * + * If we don't do this and we publish `foo@2.3.4-rc.0`, then we can't + * download `foo@2.3.0` anymore because by default CodeArtifact will + * block different versions from the same package. + */ + public async markAllUpstreamAllow() { + for await (const pkg of this.listPackages({ upstream: 'BLOCK' })) { + await retryThrottled(() => + this.codeArtifact.send( + new PutPackageOriginConfigurationCommand({ + domain: this.domain, + repository: this.repositoryName, + + format: pkg.format!, + package: pkg.package!, + namespace: pkg.namespace!, + restrictions: { + publish: 'ALLOW', + upstream: 'ALLOW', + }, + }), + ), + ); + } + } + + private async ensureDomain() { + if (await this.domainExists()) { + return; + } + await this.codeArtifact.send( + new CreateDomainCommand({ + domain: this.domain, + tags: [{ key: 'testing', value: 'true' }], + }), + ); + } + + private async ensureUpstreams() { + await this.ensureRepository(this.npmUpstream, { + description: 'The upstream repository for NPM', + external: 'public:npmjs', + }); + await this.ensureRepository(this.mavenUpstream, { + description: 'The upstream repository for Maven', + external: 'public:maven-central', + }); + await this.ensureRepository(this.nugetUpstream, { + description: 'The upstream repository for NuGet', + external: 'public:nuget-org', + }); + await this.ensureRepository(this.pypiUpstream, { + description: 'The upstream repository for PyPI', + external: 'public:pypi', + }); + } + + private async ensureRepository( + name: string, + options?: { + readonly description?: string; + readonly external?: string; + readonly upstreams?: string[]; + readonly tags?: Record; + }, + ) { + if (await this.repositoryExists(name)) { + return; + } + + await this.codeArtifact.send( + new CreateRepositoryCommand({ + domain: this.domain, + repository: name, + description: options?.description, + upstreams: options?.upstreams?.map((repositoryName) => ({ repositoryName })), + tags: options?.tags ? Object.entries(options.tags).map(([key, value]) => ({ key, value })) : undefined, + }), + ); + + if (options?.external) { + const externalConnection = options.external; + await retry(() => + this.codeArtifact.send( + new AssociateExternalConnectionCommand({ + domain: this.domain, + repository: name, + externalConnection, + }), + ), + ); + } + } + + private async domainExists() { + try { + await this.codeArtifact.send(new DescribeDomainCommand({ domain: this.domain })); + return true; + } catch (e: any) { + if (e.name !== 'ResourceNotFoundException') { + throw e; + } + return false; + } + } + + private async repositoryExists(name: string) { + try { + await this.codeArtifact.send(new DescribeRepositoryCommand({ domain: this.domain, repository: name })); + return true; + } catch (e: any) { + if (e.name !== 'ResourceNotFoundException') { + throw e; + } + return false; + } + } + + private async *listPackages(filter: Pick = {}) { + let response = await retryThrottled(() => + this.codeArtifact.send( + new ListPackagesCommand({ + domain: this.domain, + repository: this.repositoryName, + ...filter, + }), + ), + ); + + while (true) { + for (const p of response.packages ?? []) { + yield p; + } + + if (!response.nextToken) { + break; + } + + response = await retryThrottled(() => + this.codeArtifact.send( + new ListPackagesCommand({ + domain: this.domain, + repository: this.repositoryName, + ...filter, + nextToken: response.nextToken, + }), + ), + ); + } + } +} + +async function retry(block: () => Promise) { + let attempts = 3; + while (true) { + try { + return await block(); + } catch (e: any) { + if (attempts-- === 0) { + throw e; + } + // eslint-disable-next-line no-console + console.debug(e.message); + await sleep(500); + } + } +} + +async function retryThrottled(block: () => Promise) { + let time = 100; + let attempts = 15; + while (true) { + try { + return await block(); + } catch (e: any) { + // eslint-disable-next-line no-console + console.debug(e.message); + if (e.name !== 'ThrottlingException') { + throw e; + } + if (attempts-- === 0) { + throw e; + } + await sleep(Math.floor(Math.random() * time)); + time *= 2; + } + } +} + +export interface LoginInformation { + readonly authToken: string; + readonly repositoryName: string; + readonly npmEndpoint: string; + readonly mavenEndpoint: string; + readonly nugetEndpoint: string; + readonly pypiEndpoint: string; +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/staging/maven.ts b/packages/@aws-cdk-testing/cli-integ/lib/staging/maven.ts new file mode 100644 index 000000000..c7da216b7 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/staging/maven.ts @@ -0,0 +1,95 @@ +/* eslint-disable no-console */ +import * as path from 'path'; +import { pathExists } from 'fs-extra'; +import type { LoginInformation } from './codeartifact'; +import { parallelShell } from './parallel-shell'; +import type { UsageDir } from './usage-dir'; +import { writeFile } from '../files'; +import { shell } from '../shell'; + +// Do not try to JIT the Maven binary +const NO_JIT = '-XX:+TieredCompilation -XX:TieredStopAtLevel=1'; + +export async function mavenLogin(login: LoginInformation, usageDir: UsageDir) { + await writeMavenSettingsFile(settingsFile(usageDir), login); + + // Write env var + // Twiddle JVM settings a bit to make Maven survive running on a CodeBuild box. + await usageDir.addToEnv({ + MAVEN_OPTS: `-Duser.home=${usageDir.directory} ${NO_JIT} ${process.env.MAVEN_OPTS ?? ''}`.trim(), + }); +} + +function settingsFile(usageDir: UsageDir) { + // If we configure usageDir as a fake home directory Maven will find this file. + // (No other way to configure the settings file as part of the environment). + return path.join(usageDir.directory, '.m2', 'settings.xml'); +} + +export async function uploadJavaPackages(packages: string[], login: LoginInformation, usageDir: UsageDir) { + await parallelShell(packages, async (pkg, output) => { + console.log(`⏳ ${pkg}`); + + const sourcesFile = pkg.replace(/.pom$/, '-sources.jar'); + const javadocFile = pkg.replace(/.pom$/, '-javadoc.jar'); + + await shell(['mvn', + `--settings=${settingsFile(usageDir)}`, + 'org.apache.maven.plugins:maven-deploy-plugin:3.0.0:deploy-file', + `-Durl=${login.mavenEndpoint}`, + '-DrepositoryId=codeartifact', + `-DpomFile=${pkg}`, + `-Dfile=${pkg.replace(/.pom$/, '.jar')}`, + ...await pathExists(sourcesFile) ? [`-Dsources=${sourcesFile}`] : [], + ...await pathExists(javadocFile) ? [`-Djavadoc=${javadocFile}`] : []], { + outputs: [output], + modEnv: { + // Do not try to JIT the Maven binary + MAVEN_OPTS: `${NO_JIT} ${process.env.MAVEN_OPTS ?? ''}`.trim(), + }, + }); + + console.log(`✅ ${pkg}`); + }, + (pkg, output) => { + if (output.toString().includes('409 Conflict')) { + console.log(`❌ ${pkg}: already exists. Skipped.`); + return 'skip'; + } + if (output.toString().includes('Too Many Requests')) { + console.log(`♻️ ${pkg}: Too many requests. Retrying.`); + return 'retry'; + } + return 'fail'; + }); +} + +export async function writeMavenSettingsFile(filename: string, login: LoginInformation) { + await writeFile(filename, ` + + + + codeartifact + aws + ${login.authToken} + + + + + default + + + codeartifact + ${login.mavenEndpoint} + + + + + + default + + `); +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/staging/npm.ts b/packages/@aws-cdk-testing/cli-integ/lib/staging/npm.ts new file mode 100644 index 000000000..fb8fa1c38 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/staging/npm.ts @@ -0,0 +1,62 @@ +/* eslint-disable no-console */ +import * as path from 'path'; +import type { LoginInformation } from './codeartifact'; +import { parallelShell } from './parallel-shell'; +import type { UsageDir } from './usage-dir'; +import { updateIniKey, loadLines, writeLines } from '../files'; +import { shell } from '../shell'; + +export async function npmLogin(login: LoginInformation, usageDir: UsageDir) { + // Creating an ~/.npmrc that references an envvar is what you're supposed to do. (https://docs.npmjs.com/private-modules/ci-server-config) + await writeNpmLoginToken(usageDir, login.npmEndpoint, '${NPM_TOKEN}'); + + // Add variables to env file + await usageDir.addToEnv(npmEnv(usageDir, login)); +} + +function npmEnv(usageDir: UsageDir, login: LoginInformation) { + return { + npm_config_userconfig: path.join(usageDir.directory, '.npmrc'), + npm_config_registry: login.npmEndpoint, + npm_config_always_auth: 'true', // Necessary for NPM 6, otherwise it will sometimes not pass the token + NPM_TOKEN: login.authToken, + }; +} + +export async function uploadNpmPackages(packages: string[], login: LoginInformation, usageDir: UsageDir) { + await parallelShell(packages, async (pkg, output) => { + console.log(`⏳ ${pkg}`); + + // path.resolve() is required -- if the filename ends up looking like `js/bla.tgz` then NPM thinks it's a short form GitHub name. + await shell(['node', require.resolve('npm'), 'publish', path.resolve(pkg)], { + modEnv: npmEnv(usageDir, login), + show: 'error', + outputs: [output], + }); + + console.log(`✅ ${pkg}`); + }, (pkg, output) => { + if (output.toString().includes('code EPUBLISHCONFLICT')) { + console.log(`❌ ${pkg}: already exists. Skipped.`); + return 'skip'; + } + if (output.toString().includes('code EPRIVATE')) { + console.log(`❌ ${pkg}: is private. Skipped.`); + return 'skip'; + } + return 'fail'; + }); +} + +async function writeNpmLoginToken(usageDir: UsageDir, endpoint: string, token: string) { + const rcFile = path.join(usageDir.directory, '.npmrc'); + const lines = await loadLines(rcFile); + + const key = `${endpoint.replace(/^https:/, '')}:_authToken`; + updateIniKey(lines, key, token); + + await writeLines(rcFile, lines); + return rcFile; +} + +// Environment variable, .npmrc in same directory as package.json or in home dir diff --git a/packages/@aws-cdk-testing/cli-integ/lib/staging/nuget.ts b/packages/@aws-cdk-testing/cli-integ/lib/staging/nuget.ts new file mode 100644 index 000000000..ce168666a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/staging/nuget.ts @@ -0,0 +1,75 @@ +/* eslint-disable no-console */ +import type { LoginInformation } from './codeartifact'; +import { parallelShell } from './parallel-shell'; +import type { UsageDir } from './usage-dir'; +import { writeFile } from '../files'; +import { shell } from '../shell'; + +export async function nugetLogin(login: LoginInformation, usageDir: UsageDir) { + // NuGet.Config MUST live in the current directory or in the home directory, and there is no environment + // variable to configure its location. + await writeNuGetConfigFile(usageDir.cwdFile('NuGet.Config'), login); +} + +export async function uploadDotnetPackages(packages: string[], usageDir: UsageDir) { + await usageDir.copyCwdFileHere('NuGet.Config'); + + await parallelShell(packages, async (pkg, output) => { + console.log(`⏳ ${pkg}`); + + await shell(['dotnet', 'nuget', 'push', + pkg, + '--source', 'CodeArtifact', + '--no-symbols', + '--force-english-output', + '--disable-buffering', + '--timeout', '600', + '--skip-duplicate'], { + outputs: [output], + }); + + console.log(`✅ ${pkg}`); + }, + (pkg, output) => { + if (output.toString().includes('Conflict')) { + console.log(`❌ ${pkg}: already exists. Skipped.`); + return 'skip'; + } + if (output.includes('System.Threading.AbandonedMutexException')) { + console.log(`♻️ ${pkg}: AbandonedMutexException. Probably a sign of throttling, retrying.`); + return 'retry'; + } + if (output.includes('Too Many Requests')) { + console.log(`♻️ ${pkg}: Too many requests. Retrying.`); + return 'retry'; + } + if (output.includes('System.IO.IOException: The system cannot open the device or file specified.')) { + console.log(`♻️ ${pkg}: Some error that we've seen before as a result of throttling. Retrying.`); + return 'retry'; + } + return 'fail'; + }); +} + +async function writeNuGetConfigFile(filename: string, login: LoginInformation) { + // `dotnet nuget push` has an `--api-key` parameter, but CodeArtifact + // does not support that. We must authenticate with Basic auth. + await writeFile(filename, ` + + + + + + + + + + + + + + +`); +} + +// NuGet.Config in current directory diff --git a/packages/@aws-cdk-testing/cli-integ/lib/staging/parallel-shell.ts b/packages/@aws-cdk-testing/cli-integ/lib/staging/parallel-shell.ts new file mode 100644 index 000000000..9796b638d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/staging/parallel-shell.ts @@ -0,0 +1,51 @@ +import PQueue from 'p-queue'; +import { sleep } from '../aws'; +import { MemoryStream } from '../corking'; + +export type ErrorResponse = 'fail' | 'skip' | 'retry'; + +/** + * Run a function in parallel with cached output + */ +export async function parallelShell( + inputs: A[], + block: (x: A, output: NodeJS.WritableStream) => Promise, + swallowError?: (x: A, output: string) => ErrorResponse, +) { + // Limit to 10 for now, too many instances of Maven exhaust the CodeBuild instance memory + const q = new PQueue({ concurrency: Number(process.env.CONCURRENCY) || 10 }); + await q.addAll(inputs.map(input => async () => { + let attempts = 10; + let sleepMs = 500; + while (true) { + const output = new MemoryStream(); + try { + await block(input, output); + return; + } catch (e) { + switch (swallowError?.(input, output.toString())) { + case 'skip': + return; + + case 'retry': + if (--attempts > 0) { + await sleep(Math.floor(Math.random() * sleepMs)); + sleepMs *= 2; + continue; + } + break; + + case 'fail': + case undefined: + break; + } + + // eslint-disable-next-line no-console + console.error(output.toString()); + throw e; + } + } + })); + + await q.onEmpty(); +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/staging/pypi.ts b/packages/@aws-cdk-testing/cli-integ/lib/staging/pypi.ts new file mode 100644 index 000000000..9cd632f32 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/staging/pypi.ts @@ -0,0 +1,50 @@ +/* eslint-disable no-console */ +import * as path from 'path'; +import type { LoginInformation } from './codeartifact'; +import { parallelShell } from './parallel-shell'; +import type { UsageDir } from './usage-dir'; +import { writeFile } from '../files'; +import { shell } from '../shell'; + +export async function pypiLogin(login: LoginInformation, usageDir: UsageDir) { + // Write pip config file and set environment var + await writeFile(path.join(usageDir.directory, 'pip.conf'), [ + '[global]', + `index-url = https://aws:${login.authToken}@${login.pypiEndpoint.replace(/^https:\/\//, '')}simple/`, + ].join('\n')); + await usageDir.addToEnv({ + PIP_CONFIG_FILE: `${usageDir.directory}/pip.conf`, + }); +} + +export async function uploadPythonPackages(packages: string[], login: LoginInformation) { + await shell(['pip', 'install', 'twine'], { show: 'error' }); + + // Even though twine supports uploading all packages in one go, we have to upload them + // individually since CodeArtifact does not support Twine's `--skip-existing`. Fun beans. + await parallelShell(packages, async (pkg, output) => { + console.log(`⏳ ${pkg}`); + + await shell(['twine', 'upload', '--verbose', pkg], { + modEnv: { + TWINE_USERNAME: 'aws', + TWINE_PASSWORD: login.authToken, + TWINE_REPOSITORY_URL: login.pypiEndpoint, + }, + show: 'error', + outputs: [output], + }); + + console.log(`✅ ${pkg}`); + }, (pkg, output) => { + if (output.toString().includes('This package is configured to block new versions') || output.toString().includes('409 Conflict')) { + console.log(`❌ ${pkg}: already exists. Skipped.`); + return 'skip'; + } + if (output.includes('429 Too Many Requests ')) { + console.log(`♻️ ${pkg}: 429 Too Many Requests. Retrying.`); + return 'retry'; + } + return 'fail'; + }); +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/staging/usage-dir.ts b/packages/@aws-cdk-testing/cli-integ/lib/staging/usage-dir.ts new file mode 100644 index 000000000..67531661f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/staging/usage-dir.ts @@ -0,0 +1,99 @@ +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { copyDirectoryContents, homeDir, loadLines, updateIniKey, writeLines } from '../files'; + +export const DEFAULT_USAGE_DIR = path.join(homeDir(), '.codeartifact/usage'); + +/** + * The usage directory is where we write per-session config files to access the CodeArtifact repository. + * + * Some config files may be written in a system-global location, but they will not be active unless the + * contents of this directory have been sourced/copied into the current terminal. + * + * CONTRACT + * + * There are two special entries: + * + * - `env`, a file with `key=value` entries for environment variables to set. + * - `cwd/`, a directory with files that need to be copied into the current directory before each command. + * + * Other than these, code may write tempfiles to this directory if it wants, but there is no meaning + * implied for other files. + */ +export class UsageDir { + public static default() { + return new UsageDir(DEFAULT_USAGE_DIR); + } + + public readonly envFile: string; + public readonly cwdDir: string; + + private constructor(public readonly directory: string) { + this.envFile = path.join(this.directory, 'env'); + this.cwdDir = path.join(this.directory, 'cwd'); + } + + public async clean() { + await fs.rm(this.directory, { recursive: true, force: true }); + await fs.mkdirp(path.join(this.directory, 'cwd')); + await fs.writeFile(path.join(this.directory, 'env'), '', { encoding: 'utf-8' }); + + await this.addToEnv({ + CWD_FILES_DIR: path.join(this.directory, 'cwd'), + }); + + // Write a bash helper to load these settings + await fs.writeFile(path.join(this.directory, 'activate.bash'), [ + `while read -u10 line; do [[ -z $line ]] || export "$line"; done 10<${this.directory}/env`, + 'cp -R $CWD_FILES_DIR/ .', // Copy files from directory even if it is empty + ].join('\n'), { encoding: 'utf-8' }); + } + + public async addToEnv(settings: Record) { + const lines = await loadLines(this.envFile); + for (const [k, v] of Object.entries(settings)) { + updateIniKey(lines, k, v); + } + await writeLines(this.envFile, lines); + } + + public async currentEnv(): Promise> { + const lines = await loadLines(this.envFile); + + const splitter = /^([a-zA-Z0-9_-]+)\s*=\s*(.*)$/; + + const ret: Record = {}; + for (const line of lines) { + const m = line.match(splitter); + if (m) { + ret[m[1]] = m[2]; + } + } + return ret; + } + + public cwdFile(filename: string) { + return path.join(this.cwdDir, filename); + } + + public async activateInCurrentProcess() { + for (const [k, v] of Object.entries(await this.currentEnv())) { + process.env[k] = v; + } + + await copyDirectoryContents(this.cwdDir, '.'); + } + + public async copyCwdFileHere(...filenames: string[]) { + for (const file of filenames) { + await fs.copyFile(path.join(this.cwdDir, file), file); + } + } + + public advertise() { + // eslint-disable-next-line no-console + console.log('To activate these settings in the current terminal:'); + // eslint-disable-next-line no-console + console.log(` source ${this.directory}/activate.bash`); + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-aws.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-aws.ts new file mode 100644 index 000000000..8f0d11566 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-aws.ts @@ -0,0 +1,111 @@ +import { AtmosphereClient } from '@cdklabs/cdk-atmosphere-client'; +import { AwsClients } from './aws'; +import type { TestContext } from './integ-test'; +import { ResourcePool } from './resource-pool'; +import type { DisableBootstrapContext } from './with-cdk-app'; + +export function atmosphereEnabled(): boolean { + const enabled = process.env.CDK_INTEG_ATMOSPHERE_ENABLED; + return enabled === 'true' || enabled === '1'; +} + +export function atmosphereEndpoint(): string { + const value = process.env.CDK_INTEG_ATMOSPHERE_ENDPOINT; + if (!value) { + throw new Error('CDK_INTEG_ATMOSPHERE_ENDPOINT is not defined'); + } + return value; +} + +export function atmospherePool() { + const value = process.env.CDK_INTEG_ATMOSPHERE_POOL; + if (!value) { + throw new Error('CDK_INTEG_ATMOSPHERE_POOL is not defined'); + } + return value; +} + +export type AwsContext = { readonly aws: AwsClients }; + +/** + * Higher order function to execute a block with an AWS client setup + * + * Allocate the next region from the REGION pool and dispose it afterwards. + */ +export function withAws( + block: (context: A & AwsContext & DisableBootstrapContext) => Promise, + disableBootstrap: boolean = false, +): (context: A) => Promise { + return async (context: A) => { + if (atmosphereEnabled()) { + const atmosphere = new AtmosphereClient(atmosphereEndpoint()); + const allocation = await atmosphere.acquire({ pool: atmospherePool(), requester: context.name, timeoutSeconds: 60 * 30 }); + const aws = await AwsClients.forIdentity(allocation.environment.region, { + accessKeyId: allocation.credentials.accessKeyId, + secretAccessKey: allocation.credentials.secretAccessKey, + sessionToken: allocation.credentials.sessionToken, + accountId: allocation.environment.account, + }, context.output); + + await sanityCheck(aws); + + let outcome = 'success'; + try { + return await block({ ...context, disableBootstrap, aws }); + } catch (e: any) { + outcome = 'failure'; + throw e; + } finally { + await atmosphere.release(allocation.id, outcome); + } + } else { + return regionPool().using(async (region) => { + const aws = await AwsClients.forRegion(region, context.output); + await sanityCheck(aws); + + return block({ ...context, disableBootstrap, aws }); + }); + } + }; +} + +let _regionPool: undefined | ResourcePool; +export function regionPool(): ResourcePool { + if (_regionPool !== undefined) { + return _regionPool; + } + + const REGIONS = process.env.AWS_REGIONS + ? process.env.AWS_REGIONS.split(',') + : [process.env.AWS_REGION ?? process.env.AWS_DEFAULT_REGION ?? 'us-east-1']; + + _regionPool = ResourcePool.withResources('aws_regions', REGIONS); + return _regionPool; +} + +/** + * Perform a one-time quick sanity check that the AWS clients have properly configured credentials + * + * If we don't do this, calls are going to fail and they'll be retried and everything will take + * forever before the user notices a simple misconfiguration. + * + * We can't check for the presence of environment variables since credentials could come from + * anywhere, so do simple account retrieval. + * + * Only do it once per process. + */ +async function sanityCheck(aws: AwsClients) { + if (sanityChecked === undefined) { + try { + await aws.account(); + sanityChecked = true; + } catch (e: any) { + sanityChecked = false; + throw new Error(`AWS credentials probably not configured, got error: ${e.message}`); + } + } + if (!sanityChecked) { + throw new Error('AWS credentials probably not configured, see previous error'); + } +} +let sanityChecked: boolean | undefined; diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts new file mode 100644 index 000000000..754bd1506 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cdk-app.ts @@ -0,0 +1,847 @@ +/* eslint-disable no-console */ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import type { Stack } from '@aws-sdk/client-cloudformation'; +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr-public'; +import type { AwsClients } from './aws'; +import { outputFromStack, sleep } from './aws'; +import type { TestContext } from './integ-test'; +import { findYarnPackages } from './package-sources/repo-source'; +import type { IPackageSource } from './package-sources/source'; +import { packageSourceInSubprocess } from './package-sources/subprocess'; +import { RESOURCES_DIR } from './resources'; +import type { ShellOptions } from './shell'; +import { shell, ShellHelper, rimraf } from './shell'; +import type { AwsContext } from './with-aws'; +import { atmosphereEnabled, withAws } from './with-aws'; +import { withTimeout } from './with-timeout'; + +export const DEFAULT_TEST_TIMEOUT_S = 20 * 60; +export const EXTENDED_TEST_TIMEOUT_S = 30 * 60; + +/** + * Higher order function to execute a block with a CDK app fixture + * + * Requires an AWS client to be passed in. + * + * For backwards compatibility with existing tests (so we don't have to change + * too much) the inner block is expected to take a `TestFixture` object. + */ +export function withSpecificCdkApp( + appName: string, + block: (context: TestFixture) => Promise, +): (context: TestContext & AwsContext & DisableBootstrapContext) => Promise { + return async (context: TestContext & AwsContext & DisableBootstrapContext) => { + const randy = context.randomString; + const stackNamePrefix = `cdktest-${randy}`; + const integTestDir = path.join(os.tmpdir(), `cdk-integ-${randy}`); + + context.output.write(` Stack prefix: ${stackNamePrefix}\n`); + context.output.write(` Test directory: ${integTestDir}\n`); + context.output.write(` Region: ${context.aws.region}\n`); + + await cloneDirectory(path.join(RESOURCES_DIR, 'cdk-apps', appName), integTestDir, context.output); + const fixture = new TestFixture( + integTestDir, + stackNamePrefix, + context.output, + context.aws, + context.randomString); + + let success = true; + try { + const installationVersion = fixture.packages.requestedFrameworkVersion(); + + if (fixture.packages.majorVersion() === '1') { + await installNpmPackages(fixture, { + '@aws-cdk/core': installationVersion, + '@aws-cdk/aws-sns': installationVersion, + '@aws-cdk/aws-sqs': installationVersion, + '@aws-cdk/aws-iam': installationVersion, + '@aws-cdk/aws-lambda': installationVersion, + '@aws-cdk/aws-ssm': installationVersion, + '@aws-cdk/aws-ecr-assets': installationVersion, + '@aws-cdk/aws-cloudformation': installationVersion, + '@aws-cdk/aws-ec2': installationVersion, + '@aws-cdk/aws-s3': installationVersion, + 'constructs': '^3', + }); + } else { + await installNpmPackages(fixture, { + 'aws-cdk-lib': installationVersion, + 'constructs': '^10', + }); + } + + if (!context.disableBootstrap) { + await ensureBootstrapped(fixture); + } + + await block(fixture); + } catch (e) { + success = false; + throw e; + } finally { + if (process.env.INTEG_NO_CLEAN) { + context.log(`Left test directory in '${integTestDir}' ($INTEG_NO_CLEAN)\n`); + } else { + await fixture.dispose(success); + } + } + }; +} + +/** + * Like `withSpecificCdkApp`, but uses the default integration testing app with a million stacks in it + */ +export function withCdkApp( + block: (context: TestFixture) => Promise, +): (context: TestContext & AwsContext & DisableBootstrapContext) => Promise { + // 'app' is the name of the default integration app in the `cdk-apps` directory + return withSpecificCdkApp('app', block); +} + +export function withCdkMigrateApp( + language: string, + block: (context: TestFixture) => Promise, +): (context: TestContext & AwsContext & DisableBootstrapContext) => Promise { + return async (context: TestContext & AwsContext & DisableBootstrapContext) => { + const stackName = `cdk-migrate-${language}-integ-${context.randomString}`; + const integTestDir = path.join(os.tmpdir(), `cdk-migrate-${language}-integ-${context.randomString}`); + + context.output.write(` Stack name: ${stackName}\n`); + context.output.write(` Test directory: ${integTestDir}\n`); + + fs.mkdirSync(integTestDir); + const fixture = new TestFixture( + integTestDir, + stackName, + context.output, + context.aws, + context.randomString, + ); + + await ensureBootstrapped(fixture); + + await fixture.cdkMigrate(language, stackName); + + const testFixture = new TestFixture( + path.join(integTestDir, stackName), + stackName, + context.output, + context.aws, + context.randomString, + ); + + let success = true; + try { + await block(testFixture); + } catch (e) { + success = false; + throw e; + } finally { + if (process.env.INTEG_NO_CLEAN) { + context.log(`Left test directory in '${integTestDir}' ($INTEG_NO_CLEAN)`); + } else { + await fixture.dispose(success); + } + } + }; +} + +/** + * Default test fixture for most (all?) integ tests + * + * It's a composition of withAws/withCdkApp, expecting the test block to take a `TestFixture` + * object. + * + * We could have put `withAws(withCdkApp(fixture => { /... actual test here.../ }))` in every + * test declaration but centralizing it is going to make it convenient to modify in the future. + */ +export function withDefaultFixture(block: (context: TestFixture) => Promise) { + return withAws(withTimeout(DEFAULT_TEST_TIMEOUT_S, withCdkApp(block))); +} + +export function withSpecificFixture(appName: string, block: (context: TestFixture) => Promise) { + return withAws(withTimeout(DEFAULT_TEST_TIMEOUT_S, withSpecificCdkApp(appName, block))); +} + +export function withExtendedTimeoutFixture(block: (context: TestFixture) => Promise) { + return withAws(withTimeout(EXTENDED_TEST_TIMEOUT_S, withCdkApp(block))); +} + +export function withCDKMigrateFixture(language: string, block: (content: TestFixture) => Promise) { + return withAws(withTimeout(DEFAULT_TEST_TIMEOUT_S, withCdkMigrateApp(language, block))); +} + +export interface DisableBootstrapContext { + /** + * Whether to disable creating the default bootstrap + * stack prior to running the test + * + * This should be set to true when running tests that + * explicitly create a bootstrap stack + * + * @default false + */ + readonly disableBootstrap?: boolean; +} + +/** + * To be used in place of `withDefaultFixture` when the test + * should not create the default bootstrap stack + */ +export function withoutBootstrap(block: (context: TestFixture) => Promise) { + return withAws(withCdkApp(block), true); +} + +export interface CdkCliOptions extends ShellOptions { + options?: string[]; + neverRequireApproval?: boolean; + verbose?: boolean; +} + +export interface CdkDestroyCliOptions extends CdkCliOptions { + readonly force?: boolean; +} + +/** + * Prepare a target dir byreplicating a source directory + */ +export async function cloneDirectory(source: string, target: string, output?: NodeJS.WritableStream) { + await shell(['rm', '-rf', target], { outputs: output ? [output] : [] }); + await shell(['mkdir', '-p', target], { outputs: output ? [output] : [] }); + await shell(['cp', '-R', source + '/*', target], { outputs: output ? [output] : [] }); +} + +interface CommonCdkBootstrapCommandOptions { + /** + * Path to a custom bootstrap template. + * + * @default - the default CDK bootstrap template. + */ + readonly bootstrapTemplate?: string; + + readonly toolkitStackName: string; + + /** + * @default false + */ + readonly verbose?: boolean; + + /** + * @default - auto-generated CloudFormation name + */ + readonly bootstrapBucketName?: string; + + readonly cliOptions?: CdkCliOptions; + + /** + * @default - none + */ + readonly tags?: string; + + /** + * @default - the default CDK qualifier + */ + readonly qualifier?: string; +} + +export interface CdkLegacyBootstrapCommandOptions extends CommonCdkBootstrapCommandOptions { + /** + * @default false + */ + readonly noExecute?: boolean; + + /** + * @default true + */ + readonly publicAccessBlockConfiguration?: boolean; +} + +export interface CdkModernBootstrapCommandOptions extends CommonCdkBootstrapCommandOptions { + /** + * @default false + */ + readonly force?: boolean; + + /** + * @default - none + */ + readonly cfnExecutionPolicy?: string; + + /** + * @default false + */ + readonly showTemplate?: boolean; + + readonly template?: string; + + /** + * @default false + */ + readonly terminationProtection?: boolean; + + /** + * @default undefined + */ + readonly examplePermissionsBoundary?: boolean; + + /** + * @default undefined + */ + readonly customPermissionsBoundary?: string; + + /** + * @default undefined + */ + readonly usePreviousParameters?: boolean; + + readonly trust?: string[]; + + readonly untrust?: string[]; +} + +export interface CdkGarbageCollectionCommandOptions { + /** + * The amount of days an asset should stay isolated before deletion, to + * guard against some pipeline rollback scenarios + * + * @default 0 + */ + readonly rollbackBufferDays?: number; + + /** + * The type of asset that is getting garbage collected. + * + * @default 'all' + */ + readonly type?: 'ecr' | 's3' | 'all'; + + /** + * The name of the bootstrap stack + * + * @default 'CdkToolkit' + */ + readonly bootstrapStackName?: string; +} + +export class TestFixture extends ShellHelper { + public readonly qualifier: string; + private readonly bucketsToDelete = new Array(); + public readonly packages: IPackageSource; + + constructor( + public readonly integTestDir: string, + public readonly stackNamePrefix: string, + public readonly output: NodeJS.WritableStream, + public readonly aws: AwsClients, + public readonly randomString: string) { + super(integTestDir, output); + + this.qualifier = this.randomString.slice(0, 10); + this.packages = packageSourceInSubprocess(); + } + + public log(s: string) { + this.output.write(`${s}\n`); + } + + /** + * Login to the public ECR gallery using the current AWS credentials. + * Use this if your test needs to directly pull images outside of a `cdk` or `cdk-assets` command. + */ + public async ecrPublicLogin() { + const tokenResponse = await this.aws.ecrPublic.send(new GetAuthorizationTokenCommand({})); + const authData = tokenResponse.authorizationData?.authorizationToken; + + const docker = process.env.CDK_DOCKER ?? 'docker'; + + if (!authData) { + throw new Error('Could not retrieve ECR public auth token.'); + } + + const decoded = Buffer.from(authData, 'base64').toString('utf-8'); + const [username, password] = decoded.split(':'); + + await this.shell([docker, 'login', + '--username', username, + '--password', '${ECR_PASSWORD}', + 'public.ecr.aws'], { + shell: true, + modEnv: { + ECR_PASSWORD: password, + }, + }); + } + + public async cdkDeploy(stackNames: string | string[], options: CdkCliOptions = {}, skipStackRename?: boolean) { + return this.cdk(this.cdkDeployCommandLine(stackNames, options, skipStackRename), options); + } + + public cdkDeployCommandLine(stackNames: string | string[], options: CdkCliOptions = {}, skipStackRename?: boolean) { + stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + const neverRequireApproval = options.neverRequireApproval ?? true; + + return [ + 'deploy', + ...(neverRequireApproval ? ['--require-approval=never'] : []), // Default to no approval in an unattended test + ...(options.options ?? []), + // use events because bar renders bad in tests + '--progress', 'events', + ...(skipStackRename ? stackNames : this.fullStackName(stackNames)), + ]; + } + + public async cdkSynth(options: CdkCliOptions = {}) { + return this.cdk([ + 'synth', + ...(options.options ?? []), + ], options); + } + + public async cdkDestroy(stackNames: string | string[], options: CdkDestroyCliOptions = {}) { + stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + + // default to true because most tests don't test user interaction + const force = options.force ?? true; + + return this.cdk(['destroy', + ...(force ? ['-f'] : []), // pass -f if user interaction is not desired + ...(options.options ?? []), + ...this.fullStackName(stackNames)], options); + } + + public async cdkBootstrapLegacy(options: CdkLegacyBootstrapCommandOptions): Promise { + const args = ['bootstrap']; + + if (options.verbose) { + args.push('-v'); + } + args.push('--toolkit-stack-name', options.toolkitStackName); + if (options.bootstrapBucketName) { + args.push('--bootstrap-bucket-name', options.bootstrapBucketName); + } + if (options.noExecute) { + args.push('--no-execute'); + } + if (options.publicAccessBlockConfiguration !== undefined) { + args.push('--public-access-block-configuration', options.publicAccessBlockConfiguration.toString()); + } + if (options.tags) { + args.push('--tags', options.tags); + } + + return this.cdk(args, { + ...options.cliOptions, + modEnv: { + ...options.cliOptions?.modEnv, + // so that this works for V2, + // where the "new" bootstrap is the default + CDK_LEGACY_BOOTSTRAP: '1', + }, + }); + } + + public async cdkBootstrapModern(options: CdkModernBootstrapCommandOptions): Promise { + const args = ['bootstrap']; + + if (options.verbose) { + args.push('-v'); + } + if (options.showTemplate) { + args.push('--show-template'); + } + if (options.template) { + args.push('--template', options.template); + } + args.push('--toolkit-stack-name', options.toolkitStackName); + if (options.bootstrapBucketName) { + args.push('--bootstrap-bucket-name', options.bootstrapBucketName); + } + args.push('--qualifier', options.qualifier ?? this.qualifier); + if (options.cfnExecutionPolicy) { + args.push('--cloudformation-execution-policies', options.cfnExecutionPolicy); + } + if (options.terminationProtection !== undefined) { + args.push('--termination-protection', options.terminationProtection.toString()); + } + if (options.force) { + args.push('--force'); + } + if (options.tags) { + args.push('--tags', options.tags); + } + if (options.customPermissionsBoundary !== undefined) { + args.push('--custom-permissions-boundary', options.customPermissionsBoundary); + } else if (options.examplePermissionsBoundary !== undefined) { + args.push('--example-permissions-boundary'); + } + if (options.usePreviousParameters === false) { + args.push('--no-previous-parameters'); + } + if (options.bootstrapTemplate) { + args.push('--template', options.bootstrapTemplate); + } + + if (options.trust != null) { + args.push('--trust', options.trust.join(',')); + } + if (options.untrust != null) { + args.push('--untrust', options.untrust.join(',')); + } + + return this.cdk(args, { + ...options.cliOptions, + modEnv: { + ...options.cliOptions?.modEnv, + // so that this works for V1, + // where the "old" bootstrap is the default + CDK_NEW_BOOTSTRAP: '1', + }, + }); + } + + public async cdkGarbageCollect(options: CdkGarbageCollectionCommandOptions): Promise { + const args = [ + 'gc', + '--unstable=gc', // TODO: remove when stabilizing + '--confirm=false', + '--created-buffer-days=0', // Otherwise all assets created during integ tests are too young + ]; + if (options.rollbackBufferDays) { + args.push('--rollback-buffer-days', String(options.rollbackBufferDays)); + } + if (options.type) { + args.push('--type', options.type); + } + if (options.bootstrapStackName) { + args.push('--bootstrapStackName', options.bootstrapStackName); + } + + return this.cdk(args); + } + + public async cdkMigrate(language: string, stackName: string, inputPath?: string, options?: CdkCliOptions) { + return this.cdk([ + 'migrate', + '--language', + language, + '--stack-name', + stackName, + '--from-path', + inputPath ?? path.join(__dirname, '..', 'resources', 'templates', 'sqs-template.json').toString(), + ...(options?.options ?? []), + ], options); + } + + public async cdk(args: string[], options: CdkCliOptions = {}) { + const verbose = options.verbose ?? true; + + await this.packages.makeCliAvailable(); + + return this.shell(['cdk', ...(verbose ? ['-v'] : []), ...args], { + ...options, + modEnv: { + ...this.cdkShellEnv(), + ...options.modEnv, + }, + }); + } + + /** + * Return the environment variables with which to execute CDK + */ + public cdkShellEnv() { + // if tests are using an explicit aws identity already (i.e creds) + // force every cdk command to use the same identity. + const awsCreds = this.aws.identityEnv() ?? {}; + + return { + AWS_REGION: this.aws.region, + AWS_DEFAULT_REGION: this.aws.region, + STACK_NAME_PREFIX: this.stackNamePrefix, + PACKAGE_LAYOUT_VERSION: this.packages.majorVersion(), + // In these tests we want to make a distinction between stdout and sterr + CI: 'false', + ...awsCreds, + }; + } + + public template(stackName: string): any { + const fullStackName = this.fullStackName(stackName); + const templatePath = path.join(this.integTestDir, 'cdk.out', `${fullStackName}.template.json`); + return JSON.parse(fs.readFileSync(templatePath, { encoding: 'utf-8' }).toString()); + } + + public async bootstrapRepoName(): Promise { + await ensureBootstrapped(this); + + const response = await this.aws.cloudFormation.send(new DescribeStacksCommand({})); + + const stack = (response.Stacks ?? []) + .filter((s) => s.StackName && s.StackName == this.bootstrapStackName); + assert(stack.length == 1); + return outputFromStack('ImageRepositoryName', stack[0]) ?? ''; + } + + public get bootstrapStackName() { + return this.fullStackName('bootstrap-stack'); + } + + public fullStackName(stackName: string): string; + public fullStackName(stackNames: string[]): string[]; + public fullStackName(stackNames: string | string[]): string | string[] { + if (typeof stackNames === 'string') { + return `${this.stackNamePrefix}-${stackNames}`; + } else { + return stackNames.map(s => `${this.stackNamePrefix}-${s}`); + } + } + + /** + * Append this to the list of buckets to potentially delete + * + * At the end of a test, we clean up buckets that may not have gotten destroyed + * (for whatever reason). + */ + public rememberToDeleteBucket(bucketName: string) { + this.bucketsToDelete.push(bucketName); + } + + /** + * Cleanup leftover stacks and bootstrapped resources + */ + public async dispose(success: boolean) { + // when using the atmosphere service, it does resource cleanup on our behalf + // so we don't have to wait for it. + if (!atmosphereEnabled()) { + const stacksToDelete = await this.deleteableStacks(this.stackNamePrefix); + + this.sortBootstrapStacksToTheEnd(stacksToDelete); + + // Bootstrap stacks have buckets that need to be cleaned + const bucketNames = stacksToDelete.map(stack => outputFromStack('BucketName', stack)).filter(defined); + // Parallelism will be reasonable + // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism + await Promise.all(bucketNames.map(b => this.aws.emptyBucket(b))); + // The bootstrap bucket has a removal policy of RETAIN by default, so add it to the buckets to be cleaned up. + this.bucketsToDelete.push(...bucketNames); + + // Bootstrap stacks have ECR repositories with images which should be deleted + const imageRepositoryNames = stacksToDelete.map(stack => outputFromStack('ImageRepositoryName', stack)).filter(defined); + // Parallelism will be reasonable + // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism + await Promise.all(imageRepositoryNames.map(r => this.aws.deleteImageRepository(r))); + + await this.aws.deleteStacks( + ...stacksToDelete.map((s) => { + if (!s.StackName) { + throw new Error('Stack name is required to delete a stack.'); + } + return s.StackName; + }), + ); + + // We might have leaked some buckets by upgrading the bootstrap stack. Be + // sure to clean everything. + for (const bucket of this.bucketsToDelete) { + await this.aws.deleteBucket(bucket); + } + } + + // If the tests completed successfully, happily delete the fixture + // (otherwise leave it for humans to inspect) + if (success) { + const cleaned = rimraf(this.integTestDir); + if (!cleaned) { + console.error(`Failed to clean up ${this.integTestDir} due to permissions issues (Docker running as root?)`); + } + } + } + + /** + * Return the stacks starting with our testing prefix that should be deleted + */ + private async deleteableStacks(prefix: string): Promise { + const statusFilter = [ + 'CREATE_IN_PROGRESS', 'CREATE_FAILED', 'CREATE_COMPLETE', + 'ROLLBACK_IN_PROGRESS', 'ROLLBACK_FAILED', 'ROLLBACK_COMPLETE', + 'DELETE_FAILED', + 'UPDATE_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_COMPLETE', 'UPDATE_ROLLBACK_IN_PROGRESS', + 'UPDATE_ROLLBACK_FAILED', + 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_ROLLBACK_COMPLETE', 'REVIEW_IN_PROGRESS', + 'IMPORT_IN_PROGRESS', 'IMPORT_COMPLETE', + 'IMPORT_ROLLBACK_IN_PROGRESS', 'IMPORT_ROLLBACK_FAILED', + 'IMPORT_ROLLBACK_COMPLETE', + ]; + + const response = await this.aws.cloudFormation.send(new DescribeStacksCommand({})); + + return (response.Stacks ?? []) + .filter((s) => s.StackName && s.StackName.startsWith(prefix)) + .filter((s) => s.StackStatus && statusFilter.includes(s.StackStatus)) + .filter((s) => s.RootId === undefined); // Only delete parent stacks. Nested stacks are deleted in the process + } + + private sortBootstrapStacksToTheEnd(stacks: Stack[]) { + stacks.sort((a, b) => { + if (!a.StackName || !b.StackName) { + throw new Error('Stack names do not exists. These are required for sorting the bootstrap stacks.'); + } + + const aBs = a.StackName.startsWith(this.bootstrapStackName); + const bBs = b.StackName.startsWith(this.bootstrapStackName); + + return aBs != bBs + // '+' converts a boolean to 0 or 1 + ? (+aBs) - (+bBs) + : a.StackName.localeCompare(b.StackName); + }); + } +} + +/** + * Make sure that the given environment is bootstrapped + * + * Since we go striping across regions, it's going to suck doing this + * by hand so let's just mass-automate it. + */ +export async function ensureBootstrapped(fixture: TestFixture) { + // Always use the modern bootstrap stack, otherwise we may get the error + // "refusing to downgrade from version 7 to version 0" when bootstrapping with default + // settings using a v1 CLI. + // + // It doesn't matter for tests: when they want to test something about an actual legacy + // bootstrap stack, they'll create a bootstrap stack with a non-default name to test that exact property. + const envSpecifier = `aws://${await fixture.aws.account()}/${fixture.aws.region}`; + if (ALREADY_BOOTSTRAPPED_IN_THIS_RUN.has(envSpecifier)) { + return; + } + + if (atmosphereEnabled()) { + // when atmosphere is enabled, each test starts with an empty environment + // and needs to deploy the bootstrap stack. in case environments are recylced too quickly, + // cloudformation may think the bootstrap bucket still exists even though it doesnt (because of s3 eventual consistency). + // so we retry on the specific error for a while. + await bootstrapWithRetryOnBucketExists(envSpecifier, fixture); + } else { + await doBootstrap(envSpecifier, fixture, false); + } + + // when using the atmosphere service, every test needs to bootstrap + // its own environment. + if (!atmosphereEnabled()) { + ALREADY_BOOTSTRAPPED_IN_THIS_RUN.add(envSpecifier); + } +} + +async function doBootstrap(envSpecifier: string, fixture: TestFixture, allowErrExit: boolean) { + return fixture.cdk(['bootstrap', envSpecifier], { + modEnv: { + // Even for v1, use new bootstrap + CDK_NEW_BOOTSTRAP: '1', + // when allowing error exit, we probably want to inspect + // and compare output, which is better done without color characters. + ...(allowErrExit ? { FORCE_COLOR: '0' } : {}), + }, + allowErrExit, + }); +} + +async function bootstrapWithRetryOnBucketExists(envSpecifier: string, fixture: TestFixture) { + const account = await fixture.aws.account(); + const retryAfterSeconds = 30; + const bootstrapBucket = `cdk-hnb659fds-assets-${account}-${fixture.aws.region}`; + + // s3 says that a bucket deletion can take up to an hour to be fully visible. + // empirically we see that a few minutes is enough though. lets give 10 to be on the safe(r) side. + const timeoutMinutes = 10; + + const timeoutDate = new Date(Date.now() + timeoutMinutes * 60 * 1000); + while (true) { + const out = await doBootstrap(envSpecifier, fixture, true); + if (out.includes(`Environment ${envSpecifier} bootstrapped`)) { + break; + } + if (out.includes(`${bootstrapBucket} already exists`)) { + // might be an s3 eventualy consistency issue due to recycled environments. + if (Date.now() < timeoutDate.getTime()) { + fixture.log(`Bootstrap of ${envSpecifier} failed due to bucket existence check. Retrying in ${retryAfterSeconds} seconds...`); + await sleep(retryAfterSeconds * 1000); + continue; + } + } + throw new Error(`Failed bootstrapping ${envSpecifier}`); + } +} + +function defined(x: A): x is NonNullable { + return x !== undefined; +} + +/** + * Install the given NPM packages, identified by their names and versions + * + * Works by writing the packages to a `package.json` file, and + * then running NPM7's "install" on it. The use of NPM7 will automatically + * install required peerDependencies. + * + * If we're running in REPO mode and we find the package in the set of local + * packages in the repository, we'll write the directory name to `package.json` + * so that NPM will create a symlink (this allows running tests against + * built-but-unpackaged modules, and saves dev cycle time). + * + * Be aware you MUST install all the packages you directly depend upon! In the case + * of a repo/symlinking install, transitive dependencies WILL NOT be installed in the + * current directory's `node_modules` directory, because they will already have been + * symlinked from the TARGET directory's `node_modules` directory (which is sufficient + * for Node's dependency lookup mechanism). + */ +export async function installNpmPackages(fixture: TestFixture, packages: Record) { + if (process.env.REPO_ROOT) { + const monoRepo = await findYarnPackages(process.env.REPO_ROOT); + + // Replace the install target with the physical location of this package + for (const key of Object.keys(packages)) { + if (key in monoRepo) { + packages[key] = monoRepo[key]; + } + } + } + + fs.writeFileSync(path.join(fixture.integTestDir, 'package.json'), JSON.stringify({ + name: 'cdk-integ-tests', + private: true, + version: '0.0.1', + devDependencies: packages, + }, undefined, 2), { encoding: 'utf-8' }); + + // we often ECONNRESET from NPM so lets retry. this might be because of high concurrency + // which overwhelmes system resources. + const timeoutMinutes = 10; + const timeoutDate = new Date(Date.now() + timeoutMinutes * 60 * 1000); + const retryAfterSeconds = 30; + + while (true) { + try { + // Now install that `package.json` using NPM7 + await fixture.shell(['node', require.resolve('npm'), 'install']); + break; + } catch (e: any) { + if (Date.now() < timeoutDate.getTime() && fixture.output.toString().includes('ECONNRESET' )) { + fixture.log(`npm install failed due to ECONNRESET. Retrying in ${retryAfterSeconds} seconds...`); + await sleep(retryAfterSeconds * 1000); + continue; + } + throw e; + } + } +} + +const ALREADY_BOOTSTRAPPED_IN_THIS_RUN = new Set(); diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-cli-lib.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-cli-lib.ts new file mode 100644 index 000000000..ada2c84f1 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-cli-lib.ts @@ -0,0 +1,151 @@ +import * as os from 'os'; +import * as path from 'path'; +import type { TestContext } from './integ-test'; +import { RESOURCES_DIR } from './resources'; +import type { AwsContext } from './with-aws'; +import { withAws } from './with-aws'; +import type { CdkCliOptions, DisableBootstrapContext } from './with-cdk-app'; +import { cloneDirectory, installNpmPackages, TestFixture, DEFAULT_TEST_TIMEOUT_S, ensureBootstrapped } from './with-cdk-app'; +import { withTimeout } from './with-timeout'; + +/** + * Higher order function to execute a block with a CliLib Integration CDK app fixture + */ +export function withCliLibIntegrationCdkApp( + block: (context: CliLibIntegrationTestFixture) => Promise) { + return async (context: A) => { + const randy = context.randomString; + const stackNamePrefix = `cdktest-${randy}`; + const integTestDir = path.join(os.tmpdir(), `cdk-integ-${randy}`); + + context.log(` Stack prefix: ${stackNamePrefix}\n`); + context.log(` Test directory: ${integTestDir}\n`); + context.log(` Region: ${context.aws.region}\n`); + + await cloneDirectory(path.join(RESOURCES_DIR, 'cdk-apps', 'simple-app'), integTestDir, context.output); + const fixture = new CliLibIntegrationTestFixture( + integTestDir, + stackNamePrefix, + context.output, + context.aws, + context.randomString); + + let success = true; + try { + const installationVersion = fixture.packages.requestedFrameworkVersion(); + + if (fixture.packages.majorVersion() === '1') { + throw new Error('This test suite is only compatible with AWS CDK v2'); + } + + const alphaInstallationVersion = fixture.packages.requestedAlphaVersion(); + + // cli-lib-alpha has a magic alpha version in the old release pipeline, + // but will just mirror the CLI version in the new pipeline. + const cliLibVersion = process.env.CLI_LIB_VERSION_MIRRORS_CLI + ? `${fixture.packages.requestedCliVersion()}-alpha.0` + : alphaInstallationVersion; + + await installNpmPackages(fixture, { + 'aws-cdk-lib': installationVersion, + '@aws-cdk/cli-lib-alpha': cliLibVersion, + '@aws-cdk/aws-lambda-go-alpha': alphaInstallationVersion, + '@aws-cdk/aws-lambda-python-alpha': alphaInstallationVersion, + 'constructs': '^10', + }); + + if (!context.disableBootstrap) { + await ensureBootstrapped(fixture); + } + + await block(fixture); + } catch (e: any) { + // We survive certain cases involving gopkg.in + if (errorCausedByGoPkg(e.message)) { + return; + } + success = false; + throw e; + } finally { + if (process.env.INTEG_NO_CLEAN) { + context.log(`Left test directory in '${integTestDir}' ($INTEG_NO_CLEAN)\n`); + } else { + await fixture.dispose(success); + } + } + }; +} + +/** + * Return whether or not the error is being caused by gopkg.in being down + * + * Our Go build depends on https://gopkg.in/, which has errors pretty often + * (every couple of days). It is run by a single volunteer. + */ +function errorCausedByGoPkg(error: string) { + // The error is different depending on what request fails. Messages recognized: + //////////////////////////////////////////////////////////////////// + // go: github.com/aws/aws-lambda-go@v1.28.0 requires + // gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776: invalid version: git ls-remote -q origin in /go/pkg/mod/cache/vcs/0901dc1ef67fcce1c9b3ae51078740de4a0e2dc673e720584ac302973af82f36: exit status 128: + // remote: Cannot obtain refs from GitHub: cannot talk to GitHub: Get https://github.com/go-yaml/yaml.git/info/refs?service=git-upload-pack: net/http: request canceled (Client.Timeout exceeded while awaiting headers) + // fatal: unable to access 'https://gopkg.in/yaml.v3/': The requested URL returned error: 502 + //////////////////////////////////////////////////////////////////// + // go: downloading github.com/aws/aws-lambda-go v1.28.0 + // go: github.com/aws/aws-lambda-go@v1.28.0 requires + // gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776: unrecognized import path "gopkg.in/yaml.v3": reading https://gopkg.in/yaml.v3?go-get=1: 502 Bad Gateway + // server response: Cannot obtain refs from GitHub: cannot talk to GitHub: Get https://github.com/go-yaml/yaml.git/info/refs?service=git-upload-pack: net/http: request canceled (Client.Timeout exceeded while awaiting headers) + //////////////////////////////////////////////////////////////////// + // go: github.com/aws/aws-lambda-go@v1.28.0 requires + // gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776: invalid version: git fetch -f origin refs/heads/*:refs/heads/* refs/tags/*:refs/tags/* in /go/pkg/mod/cache/vcs/0901dc1ef67fcce1c9b3ae51078740de4a0e2dc673e720584ac302973af82f36: exit status 128: + // error: RPC failed; HTTP 502 curl 22 The requested URL returned error: 502 + // fatal: the remote end hung up unexpectedly + //////////////////////////////////////////////////////////////////// + + return (error.includes('gopkg\.in.*invalid version.*exit status 128') + || error.match(/unrecognized import path[^\n]gopkg\.in/)); +} + +/** + * SAM Integration test fixture for CDK - SAM integration test cases + */ +export function withCliLibFixture(block: (context: CliLibIntegrationTestFixture) => Promise) { + return withAws(withTimeout(DEFAULT_TEST_TIMEOUT_S, withCliLibIntegrationCdkApp(block))); +} + +export class CliLibIntegrationTestFixture extends TestFixture { + /** + * + */ + public async cdk(args: string[], options: CdkCliOptions = {}) { + const action = args[0]; + const stackName = args[1]; + + const cliOpts: Record = { + stacks: stackName ? [stackName] : undefined, + }; + + if (action === 'deploy') { + cliOpts.requireApproval = options.neverRequireApproval ? 'never' : 'broadening'; + } + + return this.shell(['node', '--input-type=module', `<<__EOS__ + import { AwsCdkCli } from '@aws-cdk/cli-lib-alpha'; + const cli = AwsCdkCli.fromCdkAppDirectory(); + + await cli.${action}(${JSON.stringify(cliOpts)}); +__EOS__`], { + ...options, + modEnv: { + ...this.cdkShellEnv(), + AWS_REGION: this.aws.region, + AWS_DEFAULT_REGION: this.aws.region, + STACK_NAME_PREFIX: this.stackNamePrefix, + PACKAGE_LAYOUT_VERSION: this.packages.majorVersion(), + // In these tests we want to make a distinction between stdout and sterr + CI: 'false', + ...options.modEnv, + }, + }); + } +} + diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-packages.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-packages.ts new file mode 100644 index 000000000..104d69076 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-packages.ts @@ -0,0 +1,15 @@ +import type { IPackageSource } from './package-sources/source'; +import { packageSourceInSubprocess } from './package-sources/subprocess'; + +export interface PackageContext { + readonly packages: IPackageSource; +} + +export function withPackages(block: (context: A & PackageContext) => Promise) { + return async (context: A) => { + return block({ + ...context, + packages: packageSourceInSubprocess(), + }); + }; +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-sam.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-sam.ts new file mode 100644 index 000000000..856913631 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-sam.ts @@ -0,0 +1,296 @@ +import * as child_process from 'child_process'; +import * as os from 'os'; +import * as path from 'path'; +import axios from 'axios'; +import type { TestContext } from './integ-test'; +import { RESOURCES_DIR } from './resources'; +import type { ShellOptions } from './shell'; +import { rimraf } from './shell'; +import type { AwsContext } from './with-aws'; +import { withAws } from './with-aws'; +import { cloneDirectory, installNpmPackages, TestFixture, DEFAULT_TEST_TIMEOUT_S } from './with-cdk-app'; +import { withTimeout } from './with-timeout'; + +export interface ActionOutput { + actionSucceeded?: boolean; + actionOutput?: any; + shellOutput?: string; +} + +/** + * Higher order function to execute a block with a SAM Integration CDK app fixture + */ +export function withSamIntegrationCdkApp(block: (context: SamIntegrationTestFixture) => Promise) { + return async (context: A) => { + const randy = context.randomString; + const stackNamePrefix = `cdktest-${randy}`; + const integTestDir = path.join(os.tmpdir(), `cdk-integ-${randy}`); + + context.log(` Stack prefix: ${stackNamePrefix}\n`); + context.log(` Test directory: ${integTestDir}\n`); + context.log(` Region: ${context.aws.region}\n`); + + await cloneDirectory(path.join(RESOURCES_DIR, 'cdk-apps', 'sam_cdk_integ_app'), integTestDir, context.output); + const fixture = new SamIntegrationTestFixture( + integTestDir, + stackNamePrefix, + context.output, + context.aws, + context.randomString); + + let success = true; + try { + const installationVersion = fixture.packages.requestedFrameworkVersion(); + + if (fixture.packages.majorVersion() === '1') { + await installNpmPackages(fixture, { + '@aws-cdk/aws-iam': installationVersion, + '@aws-cdk/aws-apigateway': installationVersion, + '@aws-cdk/aws-lambda': installationVersion, + '@aws-cdk/aws-lambda-go': installationVersion, + '@aws-cdk/aws-lambda-nodejs': installationVersion, + '@aws-cdk/aws-lambda-python': installationVersion, + '@aws-cdk/aws-logs': installationVersion, + '@aws-cdk/core': installationVersion, + 'constructs': '^3', + }); + } else { + const alphaInstallationVersion = fixture.packages.requestedAlphaVersion(); + await installNpmPackages(fixture, { + 'aws-cdk-lib': installationVersion, + '@aws-cdk/aws-lambda-go-alpha': alphaInstallationVersion, + '@aws-cdk/aws-lambda-python-alpha': alphaInstallationVersion, + 'constructs': '^10', + }); + } + await block(fixture); + } catch (e: any) { + // We survive certain cases involving gopkg.in + if (errorCausedByGoPkg(e.message)) { + return; + } + success = false; + throw e; + } finally { + if (process.env.INTEG_NO_CLEAN) { + context.log(`Left test directory in '${integTestDir}' ($INTEG_NO_CLEAN)\n`); + } else { + await fixture.dispose(success); + } + } + }; +} + +/** + * Return whether or not the error is being caused by gopkg.in being down + * + * Our Go build depends on https://gopkg.in/, which has errors pretty often + * (every couple of days). It is run by a single volunteer. + */ +function errorCausedByGoPkg(error: string) { + // The error is different depending on what request fails. Messages recognized: + //////////////////////////////////////////////////////////////////// + // go: github.com/aws/aws-lambda-go@v1.28.0 requires + // gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776: invalid version: git ls-remote -q origin in /go/pkg/mod/cache/vcs/0901dc1ef67fcce1c9b3ae51078740de4a0e2dc673e720584ac302973af82f36: exit status 128: + // remote: Cannot obtain refs from GitHub: cannot talk to GitHub: Get https://github.com/go-yaml/yaml.git/info/refs?service=git-upload-pack: net/http: request canceled (Client.Timeout exceeded while awaiting headers) + // fatal: unable to access 'https://gopkg.in/yaml.v3/': The requested URL returned error: 502 + //////////////////////////////////////////////////////////////////// + // go: downloading github.com/aws/aws-lambda-go v1.28.0 + // go: github.com/aws/aws-lambda-go@v1.28.0 requires + // gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776: unrecognized import path "gopkg.in/yaml.v3": reading https://gopkg.in/yaml.v3?go-get=1: 502 Bad Gateway + // server response: Cannot obtain refs from GitHub: cannot talk to GitHub: Get https://github.com/go-yaml/yaml.git/info/refs?service=git-upload-pack: net/http: request canceled (Client.Timeout exceeded while awaiting headers) + //////////////////////////////////////////////////////////////////// + // go: github.com/aws/aws-lambda-go@v1.28.0 requires + // gopkg.in/yaml.v3@v3.0.0-20200615113413-eeeca48fe776: invalid version: git fetch -f origin refs/heads/*:refs/heads/* refs/tags/*:refs/tags/* in /go/pkg/mod/cache/vcs/0901dc1ef67fcce1c9b3ae51078740de4a0e2dc673e720584ac302973af82f36: exit status 128: + // error: RPC failed; HTTP 502 curl 22 The requested URL returned error: 502 + // fatal: the remote end hung up unexpectedly + //////////////////////////////////////////////////////////////////// + + return (error.includes('gopkg\.in.*invalid version.*exit status 128') + || error.match(/unrecognized import path[^\n]gopkg\.in/)); +} + +/** + * SAM Integration test fixture for CDK - SAM integration test cases + */ +export function withSamIntegrationFixture(block: (context: SamIntegrationTestFixture) => Promise) { + return withAws(withTimeout(DEFAULT_TEST_TIMEOUT_S, withSamIntegrationCdkApp(block))); +} + +export class SamIntegrationTestFixture extends TestFixture { + public async samShell(command: string[], filter?: string, action?: () => any, options: Omit = {}): Promise { + return shellWithAction(command, filter, action, { + outputs: [this.output], + cwd: path.join(this.integTestDir, 'cdk.out').toString(), + ...options, + }); + } + + public async samBuild(stackName: string) { + const fullStackName = this.fullStackName(stackName); + const templatePath = path.join(this.integTestDir, 'cdk.out', `${fullStackName}.template.json`); + const args = ['--template', templatePath.toString()]; + return this.samShell(['sam', 'build', ...args]); + } + + public async samLocalStartApi(stackName: string, isBuilt: boolean, port: number, apiPath: string): Promise { + const fullStackName = this.fullStackName(stackName); + const templatePath = path.join(this.integTestDir, 'cdk.out', `${fullStackName}.template.json`); + const args = isBuilt? [] : ['--template', templatePath.toString()]; + args.push('--port'); + args.push(port.toString()); + + // https://github.com/aws/aws-sam-cli/pull/7892 + args.push('--no-memory-limit'); + + // "Press Ctrl+C to quit" looks to be printed by a Flask server built into SAM CLI. + return this.samShell(['sam', 'local', 'start-api', ...args], 'Press CTRL+C to quit', ()=>{ + return new Promise((resolve, reject) => { + axios.get(`http://127.0.0.1:${port}${apiPath}`).then( resp => { + resolve(resp.data); + }).catch( error => { + reject(new Error(`Failed to invoke api path ${apiPath} on port ${port} with error ${error}`)); + }); + }); + }); + } + + /** + * Cleanup leftover stacks and buckets + */ + public async dispose(success: boolean) { + // If the tests completed successfully, happily delete the fixture + // (otherwise leave it for humans to inspect) + if (success) { + const cleaned = rimraf(this.integTestDir); + if (!cleaned) { + // eslint-disable-next-line no-console + console.error(`Failed to clean up ${this.integTestDir} due to permissions issues (Docker running as root?)`); + } + } + } +} + +export function randomInteger(min: number, max: number) { + return Math.floor(Math.random() * (max - min) + min); +} + +/** + * A shell command that does what you want + * + * Is platform-aware, handles errors nicely. + */ +export async function shellWithAction( + command: string[], + filter?: string, + action?: () => Promise, + options: ShellOptions = {}, + actionTimeoutSeconds: number = 600, +): Promise { + if (options.modEnv && options.env) { + throw new Error('Use either env or modEnv but not both'); + } + + const writeToOutputs = (x: string) => { + for (const output of options.outputs ?? []) { + output.write(x); + } + }; + writeToOutputs(`💻 ${command.join(' ')}\n`); + + const env = options.env ?? (options.modEnv ? { ...process.env, ...options.modEnv } : undefined); + + const child = child_process.spawn(command[0], command.slice(1), { + ...options, + env, + // Need this for Windows where we want .cmd and .bat to be found as well. + shell: true, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + return new Promise((resolve, reject) => { + const out = new Array(); + const stdout = new Array(); + const stderr = new Array(); + let actionSucceeded = false; + let actionOutput: any; + let actionExecuted = false; + + async function maybeExecuteAction(chunk: any) { + out.push(Buffer.from(chunk)); + if (!actionExecuted && typeof filter === 'string' && Buffer.concat(out).toString('utf-8').includes(filter) && typeof action === 'function') { + actionExecuted = true; + writeToOutputs('before executing action\n'); + try { + const output = await action(); + writeToOutputs(`action output is ${JSON.stringify(output)}\n`); + actionOutput = output; + actionSucceeded = true; + } catch (error: any) { + writeToOutputs(`action error is ${error}\n`); + actionSucceeded = false; + actionOutput = error; + } finally { + writeToOutputs('terminate sam sub process\n'); + killSubProcess(child, command.join(' ')); + } + } + } + + if (typeof filter === 'string' && typeof action === 'function') { + // Reject with an error if an action is configured, but the filter failed + // to show up in the output before the timeout occurred. + setTimeout( + () => { + if (!actionExecuted) { + reject(new Error(`Timed out waiting for filter ${JSON.stringify(filter)} to appear in command output after ${actionTimeoutSeconds} seconds\nOutput so far:\n${Buffer.concat(out).toString('utf-8')}`)); + killSubProcess(child, command.join(' ')); + } + }, actionTimeoutSeconds * 1_000, + ).unref(); + } + + child.stdout!.on('data', chunk => { + writeToOutputs(chunk); + stdout.push(chunk); + void maybeExecuteAction(chunk); + }); + + child.stderr!.on('data', chunk => { + writeToOutputs(chunk); + if (options.captureStderr ?? true) { + stderr.push(chunk); + } + void maybeExecuteAction(chunk); + }); + + child.once('error', reject); + + // Wait for 'exit' instead of close, don't care about reading the streams all the way to the end + child.once('exit', (code, signal) => { + writeToOutputs(`Subprocess has exited with code ${code}, signal ${signal}\n`); + const output = (Buffer.concat(stdout).toString('utf-8') + Buffer.concat(stderr).toString('utf-8')).trim(); + if (code == null || code === 0 || options.allowErrExit) { + let result = new Array(); + result.push(actionOutput); + result.push(output); + resolve({ + actionSucceeded: actionSucceeded, + actionOutput: actionOutput, + shellOutput: output, + }); + } else { + reject(new Error(`'${command.join(' ')}' exited with error code ${code}. Output: \n${output}`)); + } + }); + }); +} + +function killSubProcess(child: child_process.ChildProcess, command: string) { + /** + * Check if the sub process is running in container, so child_process.spawn will + * create multiple processes, and to kill all of them we need to run different logic + */ + child.kill('SIGINT'); + child_process.exec(`for pid in $(ps -ef | grep "${command}" | awk '{print $2}'); do kill -2 $pid; done`); +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-temporary-directory.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-temporary-directory.ts new file mode 100644 index 000000000..f88b4b033 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-temporary-directory.ts @@ -0,0 +1,35 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import type { TestContext } from './integ-test'; +import { rimraf } from './shell'; + +export interface TemporaryDirectoryContext { + readonly integTestDir: string; +} + +export function withTemporaryDirectory(block: (context: A & TemporaryDirectoryContext) => Promise) { + return async (context: A) => { + const integTestDir = path.join(os.tmpdir(), `cdk-integ-${context.randomString}`); + + fs.mkdirSync(integTestDir, { recursive: true }); + + try { + await block({ + ...context, + integTestDir, + }); + + // Clean up in case of success + if (process.env.SKIP_CLEANUP) { + context.log(`Left test directory in '${integTestDir}' ($SKIP_CLEANUP)\n`); + } else { + rimraf(integTestDir); + } + } catch (e) { + context.log(`Left test directory in '${integTestDir}'\n`); + throw e; + } + }; +} + diff --git a/packages/@aws-cdk-testing/cli-integ/lib/with-timeout.ts b/packages/@aws-cdk-testing/cli-integ/lib/with-timeout.ts new file mode 100644 index 000000000..76aa70336 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/with-timeout.ts @@ -0,0 +1,33 @@ +/** + * Run a block with a timeout + * + * We can't use the jest timeout feature: + * + * - `jest.concurrent()` does not do any concurrency management. It starts all + * tests at the same time. + * - Our tests use locking to make sure only one test is running at a time per + * region. + * + * The wait time for the locks is included in the jest test timeout. We therefore + * need to set it unreasonably high (as long as the last test may need to wait + * if all tests are executed using only 1 region, and they effectively execute + * sequentially), which makes it not useful to detect stuck tests. + * + * The `withTimeout()` modifier makes it possible to measure only a specific + * block of code. In our case: the effective test code, excluding the wait time. + */ +export function withTimeout(seconds: number, block: (x: A) => Promise) { + return (x: A) => { + const timeOut = new Promise((_ok, ko) => { + const timerHandle = setTimeout( + () => ko(new Error(`Timeout: test took more than ${seconds}s to complete`)), + seconds * 1000); + timerHandle.unref(); + }); + + return Promise.race([ + block(x), + timeOut, + ]); + }; +} diff --git a/packages/@aws-cdk-testing/cli-integ/lib/xpmutex.ts b/packages/@aws-cdk-testing/cli-integ/lib/xpmutex.ts new file mode 100644 index 000000000..372395272 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/xpmutex.ts @@ -0,0 +1,228 @@ +import { watch, promises as fs, mkdirSync } from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +export class XpMutexPool { + public static fromDirectory(directory: string) { + mkdirSync(directory, { recursive: true }); + return new XpMutexPool(directory); + } + + public static fromName(name: string) { + return XpMutexPool.fromDirectory(path.join(os.tmpdir(), name)); + } + + private readonly waitingResolvers = new Set<() => void>(); + private watcher: ReturnType | undefined; + + private constructor(public readonly directory: string) { + this.startWatch(); + } + + public mutex(name: string) { + return new XpMutex(this, name); + } + + /** + * Await an unlock event + * + * (An unlock event is when a file in the directory gets deleted, with a tiny + * random sleep attached to it). + */ + public awaitUnlock(maxWaitMs?: number): Promise { + const wait = new Promise(ok => { + this.waitingResolvers.add(async () => { + await randomSleep(10); + ok(); + }); + }); + + if (maxWaitMs) { + return Promise.race([wait, sleep(maxWaitMs)]); + } else { + return wait; + } + } + + private startWatch() { + this.watcher = watch(this.directory); + (this.watcher as any).unref(); // @types doesn't know about this but it exists + this.watcher.on('change', async (eventType, fname) => { + // Only trigger on 'deletes'. + // After receiving the event, we check if the file exists. + // - If no: the file was deleted! Huzzah, this counts as a wakeup. + // - If yes: either the file was just created (in which case we don't need to wakeup) + // or the event was due to a delete but someone raced us to it and claimed the + // file already (in which case we also don't need to wake up). + if (eventType === 'rename' && !await fileExists(path.join(this.directory, fname.toString()))) { + this.notifyWaiters(); + } + }); + this.watcher.on('error', async (e) => { + // eslint-disable-next-line no-console + console.error(e); + await randomSleep(100); + this.startWatch(); + }); + } + + private notifyWaiters() { + for (const promise of this.waitingResolvers) { + promise(); + } + this.waitingResolvers.clear(); + } +} + +/** + * Cross-process mutex + * + * Uses the presence of a file on disk and `fs.watch` to represent the mutex + * and discover unlocks. + */ +export class XpMutex { + private readonly fileName: string; + + constructor(private readonly pool: XpMutexPool, public readonly mutexName: string) { + this.fileName = path.join(pool.directory, `${mutexName}.mutex`); + } + + /** + * Try to acquire the lock (may fail) + */ + public async tryAcquire(): Promise { + while (true) { + // Acquire lock by being the one to create the file + try { + return await this.writePidFile('wx'); // Fails if the file already exists + } catch (e: any) { + if (e.code !== 'EEXIST') { + throw e; + } + } + + // File already exists. Read the contents, see if it's an existent PID (if so, the lock is taken) + const ownerPid = await this.readPidFile(); + if (ownerPid === undefined) { + // File got deleted just now, maybe we can acquire it again + continue; + } + if (processExists(ownerPid)) { + return undefined; + } + + // If not, the lock is stale and will never be released anymore. We may + // delete it and acquire it anyway, but we may be racing someone else trying + // to do the same. Solve this as follows: + // - Try to acquire a lock that gives us permissions to declare the existing lock stale. + // - Sleep a small random period to reduce contention on this operation + await randomSleep(10); + const innerMux = new XpMutex(this.pool, `${this.mutexName}.${ownerPid}`); + const innerLock = await innerMux.tryAcquire(); + if (!innerLock) { + return undefined; + } + + // We may not release the 'inner lock' we used to acquire the rights to declare the other + // lock stale until we release the actual lock itself. If we did, other contenders might + // see it released while they're still in this fallback block and accidentally steal + // from a new legitimate owner. + return this.writePidFile('w', innerLock); // Force write lock file, attach inner lock as well + } + } + + /** + * Acquire the lock, waiting until we can + */ + public async acquire(): Promise { + while (true) { + // Start the wait here, so we don't miss the signal if it comes after + // we try but before we sleep. + // + // We also periodically retry anyway since we may have missed the delete + // signal due to unfortunate timing. + const wait = this.pool.awaitUnlock(5000); + + const lock = await this.tryAcquire(); + if (lock) { + // Ignore the wait (count as handled) + wait.then(() => { + }, () => { + }); + return lock; + } + + await wait; + await randomSleep(100); + } + } + + private async readPidFile(): Promise { + const deadLine = Date.now() + 1000; + while (Date.now() < deadLine) { + let contents; + try { + contents = await fs.readFile(this.fileName, { encoding: 'utf-8' }); + } catch (e: any) { + if (e.code === 'ENOENT') { + return undefined; + } + throw e; + } + + // Retry until we've seen the full contents + if (contents.endsWith('.')) { + return parseInt(contents.substring(0, contents.length - 1), 10); + } + await sleep(10); + } + + throw new Error(`${this.fileName} was never completely written`); + } + + private async writePidFile(mode: string, additionalLock?: ILock): Promise { + const fd = await fs.open(this.fileName, mode); // May fail if the file already exists + await fd.write(`${process.pid}.`); // Period guards against partial reads + await fd.close(); + + return { + release: async () => { + await fs.unlink(this.fileName); + await additionalLock?.release(); + }, + }; + } +} + +export interface ILock { + release(): Promise; +} + +async function fileExists(fileName: string) { + try { + await fs.stat(fileName); + return true; + } catch (e: any) { + if (e.code === 'ENOENT') { + return false; + } + throw e; + } +} + +function processExists(pid: number) { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +function sleep(ms: number): Promise { + return new Promise(ok => (setTimeout(ok, ms) as any).unref()); +} + +function randomSleep(ms: number) { + return sleep(Math.floor(Math.random() * ms)); +} diff --git a/packages/@aws-cdk-testing/cli-integ/package.json b/packages/@aws-cdk-testing/cli-integ/package.json new file mode 100644 index 000000000..9af6c87d5 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/package.json @@ -0,0 +1,120 @@ +{ + "name": "@aws-cdk-testing/cli-integ", + "description": "Integration tests for the AWS CDK CLI", + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk-cli", + "directory": "packages/@aws-cdk-testing/cli-integ" + }, + "bin": { + "apply-patches": "bin/apply-patches", + "download-and-run-old-tests": "bin/download-and-run-old-tests", + "query-github": "bin/query-github", + "run-suite": "bin/run-suite", + "stage-distribution": "bin/stage-distribution", + "test-root": "bin/test-root" + }, + "scripts": { + "build": "npx projen build", + "bump": "npx projen bump", + "check-for-updates": "npx projen check-for-updates", + "check-licenses": "npx projen check-licenses", + "compile": "npx projen compile", + "default": "npx projen default", + "eslint": "npx projen eslint", + "gather-versions": "npx projen gather-versions", + "nx": "npx projen nx", + "package": "npx projen package", + "post-compile": "npx projen post-compile", + "pre-compile": "npx projen pre-compile", + "test": "npx projen test", + "test:watch": "npx projen test:watch", + "unbump": "npx projen unbump", + "watch": "npx projen watch", + "projen": "npx projen" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "devDependencies": { + "@cdklabs/eslint-plugin": "^1.3.2", + "@stylistic/eslint-plugin": "^3", + "@types/fs-extra": "^9", + "@types/glob": "^7", + "@types/jest": "^29.5.14", + "@types/node": "^16", + "@types/semver": "^7", + "@types/yargs": "^15", + "@typescript-eslint/eslint-plugin": "^8", + "@typescript-eslint/parser": "^8", + "commit-and-tag-version": "^12", + "constructs": "^10.0.0", + "eslint": "^9", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.10.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jest": "^28.11.0", + "eslint-plugin-jsdoc": "^50.6.9", + "eslint-plugin-prettier": "^5.2.6", + "jest": "^29.7.0", + "jest-junit": "^16", + "license-checker": "^25.0.1", + "prettier": "^2.8", + "projen": "^0.91.20", + "ts-jest": "^29.2.5", + "typescript": "5.6" + }, + "dependencies": { + "@aws-sdk/client-cloudformation": "^3", + "@aws-sdk/client-codeartifact": "^3", + "@aws-sdk/client-ecr": "^3", + "@aws-sdk/client-ecr-public": "^3", + "@aws-sdk/client-ecs": "^3", + "@aws-sdk/client-iam": "^3", + "@aws-sdk/client-lambda": "^3", + "@aws-sdk/client-s3": "^3", + "@aws-sdk/client-sns": "^3", + "@aws-sdk/client-sso": "^3", + "@aws-sdk/client-sts": "^3", + "@aws-sdk/credential-providers": "^3", + "@cdklabs/cdk-atmosphere-client": "^0.0.27", + "@octokit/rest": "^18.12.0", + "@smithy/types": "^3", + "@smithy/util-retry": "^3", + "axios": "^1", + "chalk": "^4", + "fs-extra": "^9", + "glob": "^7", + "jest": "^29", + "jest-junit": "^15", + "make-runnable": "^1", + "mockttp": "^3", + "node-pty": "^1.0.0", + "npm": "^10", + "p-queue": "^6", + "semver": "^7", + "sinon": "^9", + "ts-jest": "^29", + "ts-mock-imports": "^1", + "yaml": "1", + "yargs": "^17" + }, + "keywords": [ + "aws", + "cdk" + ], + "engines": { + "node": ">= 14.15.0" + }, + "main": "lib/index.js", + "license": "Apache-2.0", + "homepage": "https://github.com/aws/aws-cdk", + "publishConfig": { + "access": "public" + }, + "version": "0.0.0", + "types": "lib/index.d.ts", + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/bootstrap-templates/session-tags.all-roles-deny-all.yaml b/packages/@aws-cdk-testing/cli-integ/resources/bootstrap-templates/session-tags.all-roles-deny-all.yaml new file mode 100644 index 000000000..e2b80c535 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/bootstrap-templates/session-tags.all-roles-deny-all.yaml @@ -0,0 +1,703 @@ +Description: This stack includes resources needed to deploy AWS CDK apps into this + environment +Parameters: + TrustedAccounts: + Description: List of AWS accounts that are trusted to publish assets and deploy + stacks to this environment + Default: '' + Type: CommaDelimitedList + TrustedAccountsForLookup: + Description: List of AWS accounts that are trusted to look up values in this + environment + Default: '' + Type: CommaDelimitedList + CloudFormationExecutionPolicies: + Description: List of the ManagedPolicy ARN(s) to attach to the CloudFormation + deployment role + Default: '' + Type: CommaDelimitedList + FileAssetsBucketName: + Description: The name of the S3 bucket used for file assets + Default: '' + Type: String + FileAssetsBucketKmsKeyId: + Description: Empty to create a new key (default), 'AWS_MANAGED_KEY' to use a managed + S3 key, or the ID/ARN of an existing key. + Default: '' + Type: String + ContainerAssetsRepositoryName: + Description: A user-provided custom name to use for the container assets ECR repository + Default: '' + Type: String + Qualifier: + Description: An identifier to distinguish multiple bootstrap stacks in the same environment + Default: hnb659fds + Type: String + # "cdk-(qualifier)-image-publishing-role-(account)-(region)" needs to be <= 64 chars + # account = 12, region <= 14, 10 chars for qualifier and 28 for rest of role name + AllowedPattern: "[A-Za-z0-9_-]{1,10}" + ConstraintDescription: Qualifier must be an alphanumeric identifier of at most 10 characters + PublicAccessBlockConfiguration: + Description: Whether or not to enable S3 Staging Bucket Public Access Block Configuration + Default: 'true' + Type: 'String' + AllowedValues: ['true', 'false'] + InputPermissionsBoundary: + Description: Whether or not to use either the CDK supplied or custom permissions boundary + Default: '' + Type: 'String' + UseExamplePermissionsBoundary: + Default: 'false' + AllowedValues: [ 'true', 'false' ] + Type: String + BootstrapVariant: + Type: String + Default: 'AWS CDK: Default Resources' + Description: Describe the provenance of the resources in this bootstrap + stack. Change this when you customize the template. To prevent accidents, + the CDK CLI will not overwrite bootstrap stacks with a different variant. +Conditions: + HasTrustedAccounts: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: TrustedAccounts + HasTrustedAccountsForLookup: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: TrustedAccountsForLookup + HasCloudFormationExecutionPolicies: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: CloudFormationExecutionPolicies + HasCustomFileAssetsBucketName: + Fn::Not: + - Fn::Equals: + - '' + - Ref: FileAssetsBucketName + CreateNewKey: + Fn::Equals: + - '' + - Ref: FileAssetsBucketKmsKeyId + UseAwsManagedKey: + Fn::Equals: + - 'AWS_MANAGED_KEY' + - Ref: FileAssetsBucketKmsKeyId + ShouldCreatePermissionsBoundary: + Fn::Equals: + - 'true' + - Ref: UseExamplePermissionsBoundary + PermissionsBoundarySet: + Fn::Not: + - Fn::Equals: + - '' + - Ref: InputPermissionsBoundary + HasCustomContainerAssetsRepositoryName: + Fn::Not: + - Fn::Equals: + - '' + - Ref: ContainerAssetsRepositoryName + UsePublicAccessBlockConfiguration: + Fn::Equals: + - 'true' + - Ref: PublicAccessBlockConfiguration +Resources: + FileAssetsBucketEncryptionKey: + Type: AWS::KMS::Key + Properties: + KeyPolicy: + Statement: + - Action: + - kms:Create* + - kms:Describe* + - kms:Enable* + - kms:List* + - kms:Put* + - kms:Update* + - kms:Revoke* + - kms:Disable* + - kms:Get* + - kms:Delete* + - kms:ScheduleKeyDeletion + - kms:CancelKeyDeletion + - kms:GenerateDataKey + - kms:TagResource + - kms:UntagResource + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + Resource: "*" + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Principal: + # Not actually everyone -- see below for Conditions + AWS: "*" + Resource: "*" + Condition: + StringEquals: + # See https://docs.aws.amazon.com/kms/latest/developerguide/policy-conditions.html#conditions-kms-caller-account + kms:CallerAccount: + Ref: AWS::AccountId + kms:ViaService: + - Fn::Sub: s3.${AWS::Region}.amazonaws.com + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Principal: + AWS: + Fn::Sub: "${FilePublishingRole.Arn}" + Resource: "*" + Condition: CreateNewKey + FileAssetsBucketEncryptionKeyAlias: + Condition: CreateNewKey + Type: AWS::KMS::Alias + Properties: + AliasName: + Fn::Sub: "alias/cdk-${Qualifier}-assets-key" + TargetKeyId: + Ref: FileAssetsBucketEncryptionKey + StagingBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: + Fn::If: + - HasCustomFileAssetsBucketName + - Fn::Sub: "${FileAssetsBucketName}" + - Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region} + AccessControl: Private + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: aws:kms + KMSMasterKeyID: + Fn::If: + - CreateNewKey + - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" + - Fn::If: + - UseAwsManagedKey + - Ref: AWS::NoValue + - Fn::Sub: "${FileAssetsBucketKmsKeyId}" + PublicAccessBlockConfiguration: + Fn::If: + - UsePublicAccessBlockConfiguration + - BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + - Ref: AWS::NoValue + VersioningConfiguration: + Status: Enabled + LifecycleConfiguration: + Rules: + # Exising objects will never be overwritten but Security Hub wants this rule to exist + - Id: CleanupOldVersions + Status: Enabled + NoncurrentVersionExpiration: + NoncurrentDays: 365 + UpdateReplacePolicy: Retain + DeletionPolicy: Retain + 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: '*' + ContainerAssetsRepository: + Type: AWS::ECR::Repository + Properties: + ImageTagMutability: IMMUTABLE + # Untagged images should never exist but Security Hub wants this rule to exist + LifecyclePolicy: + LifecyclePolicyText: | + { + "rules": [ + { + "rulePriority": 1, + "description": "Untagged images should not exist, but expire any older than one year", + "selection": { + "tagStatus": "untagged", + "countType": "sinceImagePushed", + "countUnit": "days", + "countNumber": 365 + }, + "action": { "type": "expire" } + } + ] + } + RepositoryName: + Fn::If: + - HasCustomContainerAssetsRepositoryName + - Fn::Sub: "${ContainerAssetsRepositoryName}" + - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} + RepositoryPolicyText: + Version: "2012-10-17" + Statement: + # Necessary for Lambda container images + # https://docs.aws.amazon.com/lambda/latest/dg/configuration-images.html#configuration-images-permissions + - Sid: LambdaECRImageRetrievalPolicy + Effect: Allow + Principal: { Service: "lambda.amazonaws.com" } + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Condition: + StringLike: + "aws:sourceArn": { "Fn::Sub": "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*" } + FilePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + # The TagSession action is required to be able to assume this role with session tags. + # Without this trust policy, attemping to assume this role with session tags will fail. + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: file-publishing + ImagePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + # The TagSession action is required to be able to assume this role with session tags. + # Without this trust policy, attemping to assume this role with session tags will fail. + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: image-publishing + LookupRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + # The TagSession action is required to be able to assume this role with session tags. + # Without this trust policy, attemping to assume this role with session tags will fail. + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccountsForLookup + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccountsForLookup + - Ref: AWS::NoValue + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region} + Policies: + - PolicyDocument: + Statement: + - Sid: AllowEc2OnlyIfEngineeringDepartement + Effect: Allow + Action: + - ec2:* + Resource: "*" + Condition: + StringEquals: + aws:PrincipalTag/Department: "Engineering" + Version: '2012-10-17' + PolicyName: LookupRolePolicy + Tags: + - Key: aws-cdk:bootstrap-role + Value: lookup + FilePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - s3:GetObject* + - s3:GetBucket* + - s3:GetEncryptionConfiguration + - s3:List* + - s3:DeleteObject* + - s3:PutObject* + - s3:Abort* + Resource: + - Fn::Sub: "${StagingBucket.Arn}" + - Fn::Sub: "${StagingBucket.Arn}/*" + # This condition requires that the File Publishing Role is assumed with the session tags + # 'Department: Engineering'; if these tags are not passed in, the role will + # not be able to perform these S3 actions. + Condition: + StringEquals: + aws:ResourceAccount: + - Fn::Sub: ${AWS::AccountId} + aws:PrincipalTag/Department: "Engineering" + Effect: Allow + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Resource: + Fn::If: + - CreateNewKey + - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" + - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId} + Version: '2012-10-17' + Roles: + - Ref: FilePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + ImagePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + - ecr:BatchCheckLayerAvailability + - ecr:DescribeRepositories + - ecr:DescribeImages + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Resource: + Fn::Sub: "${ContainerAssetsRepository.Arn}" + Effect: Allow + # This condition requires that the Image Publishing Role is assumed with the session tags + # 'Department: Engineering'; if these tags are not passed in, the role will + # not be able to perform these ECR actions. + Condition: + StringEquals: + aws:PrincipalTag/Department: "Engineering" + - Action: + - ecr:GetAuthorizationToken + Resource: "*" + Effect: Allow + Version: '2012-10-17' + Roles: + - Ref: ImagePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + DeploymentActionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + # The TagSession action is required to be able to assume this role with session tags. + # Without this trust policy, attemping to assume this role with session tags will fail. + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + Policies: + - PolicyDocument: + Statement: + - Sid: CloudFormationPermissions + Effect: Allow + Action: + - cloudformation:CreateChangeSet + - cloudformation:DeleteChangeSet + - cloudformation:DescribeChangeSet + - cloudformation:DescribeStacks + - cloudformation:ExecuteChangeSet + - cloudformation:CreateStack + - cloudformation:UpdateStack + Resource: "*" + # This condition requires that the Deploy Role is assumed with the session tags + # 'Department: Engineering'; if these tags are not passed in, the Deploy Role will + # not be able to perform these CloudFormation actions. + Condition: + StringEquals: + aws:PrincipalTag/Department: "Engineering" + - Sid: PipelineCrossAccountArtifactsBucket + # Read/write buckets in different accounts. Permissions to buckets in + # same account are granted by bucket policies. + # + # Write permissions necessary to write outputs to the cross-region artifact replication bucket + # https://aws.amazon.com/premiumsupport/knowledge-center/codepipeline-deploy-cloudformation/. + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + - s3:Abort* + - s3:DeleteObject* + - s3:PutObject* + Resource: "*" + Condition: + StringNotEquals: + s3:ResourceAccount: + Ref: 'AWS::AccountId' + - Sid: PipelineCrossAccountArtifactsKey + # Use keys only for the purposes of reading encrypted files from S3. + Effect: Allow + Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Resource: "*" + Condition: + StringEquals: + kms:ViaService: + Fn::Sub: s3.${AWS::Region}.amazonaws.com + - Action: iam:PassRole + Resource: + Fn::Sub: "${CloudFormationExecutionRole.Arn}" + Effect: Allow + - Sid: CliPermissions + Action: + # Permissions needed by the CLI when doing `cdk deploy`. + # Our CI/CD does not need DeleteStack, + # but we also want to use this role from the CLI, + # and there you can call `cdk destroy` + - cloudformation:DescribeStackEvents + - cloudformation:GetTemplate + - cloudformation:DeleteStack + - cloudformation:UpdateTerminationProtection + - sts:GetCallerIdentity + # `cdk import` + - cloudformation:GetTemplateSummary + Resource: "*" + Effect: Allow + - Sid: CliStagingBucket + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + Resource: + - Fn::Sub: ${StagingBucket.Arn} + - Fn::Sub: ${StagingBucket.Arn}/* + - Sid: ReadVersion + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters # CreateChangeSet uses this to evaluate any SSM parameters (like `CdkBootstrapVersion`) + Resource: + - Fn::Sub: "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CdkBootstrapVersion}" + Version: '2012-10-17' + PolicyName: default + RoleName: + Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: deploy + CloudFormationExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: cloudformation.amazonaws.com + Version: '2012-10-17' + ManagedPolicyArns: + Fn::If: + - HasCloudFormationExecutionPolicies + - Ref: CloudFormationExecutionPolicies + - Fn::If: + - HasTrustedAccounts + # The CLI will prevent this case from occurring + - Ref: AWS::NoValue + # The CLI will advertise that we picked this implicitly + - - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" + RoleName: + Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} + PermissionsBoundary: + Fn::If: + - PermissionsBoundarySet + - Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}' + - Ref: AWS::NoValue + CdkBoostrapPermissionsBoundaryPolicy: + # Edit the template prior to boostrap in order to have this example policy created + Condition: ShouldCreatePermissionsBoundary + Type: AWS::IAM::ManagedPolicy + Properties: + PolicyDocument: + Statement: + # If permission boundaries do not have an explicit `allow`, then the effect is `deny` + - Sid: ExplicitAllowAll + Action: + - "*" + Effect: Allow + Resource: "*" + # Default permissions to prevent privilege escalation + - Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied + Action: + - iam:CreateUser + - iam:CreateRole + - iam:PutRolePermissionsBoundary + - iam:PutUserPermissionsBoundary + Condition: + StringNotEquals: + iam:PermissionsBoundary: + Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + Effect: Deny + Resource: "*" + # Forbid the policy itself being edited + - Sid: DenyPermBoundaryIAMPolicyAlteration + Action: + - iam:CreatePolicyVersion + - iam:DeletePolicy + - iam:DeletePolicyVersion + - iam:SetDefaultPolicyVersion + Effect: Deny + Resource: + Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + # Forbid removing the permissions boundary from any user or role that has it associated + - Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole + Action: + - iam:DeleteUserPermissionsBoundary + - iam:DeleteRolePermissionsBoundary + Effect: Deny + Resource: "*" + # Add your specific organizational security policy here + # Uncomment the example to deny access to AWS Config + #- Sid: OrganizationalSecurityPolicy + # Action: + # - "config:*" + # Effect: Deny + # Resource: "*" + Version: "2012-10-17" + Description: "Bootstrap Permission Boundary" + ManagedPolicyName: + Fn::Sub: cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + Path: / + # The SSM parameter is used in pipeline-deployed templates to verify the version + # of the bootstrap resources. + CdkBootstrapVersion: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: + Fn::Sub: '/cdk-bootstrap/${Qualifier}/version' + Value: '22' +Outputs: + BucketName: + Description: The name of the S3 bucket owned by the CDK toolkit stack + Value: + Fn::Sub: "${StagingBucket}" + BucketDomainName: + Description: The domain name of the S3 bucket owned by the CDK toolkit stack + Value: + Fn::Sub: "${StagingBucket.RegionalDomainName}" + # @deprecated - This Export can be removed at some future point in time. + # We can't do it today because if there are stacks that use it, the bootstrap + # stack cannot be updated. Not used anymore by apps >= 1.60.0 + FileAssetKeyArn: + Description: The ARN of the KMS key used to encrypt the asset bucket (deprecated) + Value: + Fn::If: + - CreateNewKey + - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" + - Fn::Sub: "${FileAssetsBucketKmsKeyId}" + Export: + Name: + Fn::Sub: CdkBootstrap-${Qualifier}-FileAssetKeyArn + ImageRepositoryName: + Description: The name of the ECR repository which hosts docker image assets + Value: + Fn::Sub: "${ContainerAssetsRepository}" + # The Output is used by the CLI to verify the version of the bootstrap resources. + BootstrapVersion: + Description: The version of the bootstrap resources that are currently mastered + in this stack + Value: + Fn::GetAtt: [CdkBootstrapVersion, Value] diff --git a/packages/@aws-cdk-testing/cli-integ/resources/bootstrap-templates/session-tags.deploy-role-deny-sqs.yaml b/packages/@aws-cdk-testing/cli-integ/resources/bootstrap-templates/session-tags.deploy-role-deny-sqs.yaml new file mode 100644 index 000000000..94e789a04 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/bootstrap-templates/session-tags.deploy-role-deny-sqs.yaml @@ -0,0 +1,700 @@ +Description: This stack includes resources needed to deploy AWS CDK apps into this + environment +Parameters: + TrustedAccounts: + Description: List of AWS accounts that are trusted to publish assets and deploy + stacks to this environment + Default: '' + Type: CommaDelimitedList + TrustedAccountsForLookup: + Description: List of AWS accounts that are trusted to look up values in this + environment + Default: '' + Type: CommaDelimitedList + CloudFormationExecutionPolicies: + Description: List of the ManagedPolicy ARN(s) to attach to the CloudFormation + deployment role + Default: '' + Type: CommaDelimitedList + FileAssetsBucketName: + Description: The name of the S3 bucket used for file assets + Default: '' + Type: String + FileAssetsBucketKmsKeyId: + Description: Empty to create a new key (default), 'AWS_MANAGED_KEY' to use a managed + S3 key, or the ID/ARN of an existing key. + Default: '' + Type: String + ContainerAssetsRepositoryName: + Description: A user-provided custom name to use for the container assets ECR repository + Default: '' + Type: String + Qualifier: + Description: An identifier to distinguish multiple bootstrap stacks in the same environment + Default: hnb659fds + Type: String + # "cdk-(qualifier)-image-publishing-role-(account)-(region)" needs to be <= 64 chars + # account = 12, region <= 14, 10 chars for qualifier and 28 for rest of role name + AllowedPattern: "[A-Za-z0-9_-]{1,10}" + ConstraintDescription: Qualifier must be an alphanumeric identifier of at most 10 characters + PublicAccessBlockConfiguration: + Description: Whether or not to enable S3 Staging Bucket Public Access Block Configuration + Default: 'true' + Type: 'String' + AllowedValues: ['true', 'false'] + InputPermissionsBoundary: + Description: Whether or not to use either the CDK supplied or custom permissions boundary + Default: '' + Type: 'String' + UseExamplePermissionsBoundary: + Default: 'false' + AllowedValues: [ 'true', 'false' ] + Type: String + BootstrapVariant: + Type: String + Default: 'AWS CDK: Default Resources' + Description: Describe the provenance of the resources in this bootstrap + stack. Change this when you customize the template. To prevent accidents, + the CDK CLI will not overwrite bootstrap stacks with a different variant. +Conditions: + HasTrustedAccounts: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: TrustedAccounts + HasTrustedAccountsForLookup: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: TrustedAccountsForLookup + HasCloudFormationExecutionPolicies: + Fn::Not: + - Fn::Equals: + - '' + - Fn::Join: + - '' + - Ref: CloudFormationExecutionPolicies + HasCustomFileAssetsBucketName: + Fn::Not: + - Fn::Equals: + - '' + - Ref: FileAssetsBucketName + CreateNewKey: + Fn::Equals: + - '' + - Ref: FileAssetsBucketKmsKeyId + UseAwsManagedKey: + Fn::Equals: + - 'AWS_MANAGED_KEY' + - Ref: FileAssetsBucketKmsKeyId + ShouldCreatePermissionsBoundary: + Fn::Equals: + - 'true' + - Ref: UseExamplePermissionsBoundary + PermissionsBoundarySet: + Fn::Not: + - Fn::Equals: + - '' + - Ref: InputPermissionsBoundary + HasCustomContainerAssetsRepositoryName: + Fn::Not: + - Fn::Equals: + - '' + - Ref: ContainerAssetsRepositoryName + UsePublicAccessBlockConfiguration: + Fn::Equals: + - 'true' + - Ref: PublicAccessBlockConfiguration +Resources: + FileAssetsBucketEncryptionKey: + Type: AWS::KMS::Key + Properties: + KeyPolicy: + Statement: + - Action: + - kms:Create* + - kms:Describe* + - kms:Enable* + - kms:List* + - kms:Put* + - kms:Update* + - kms:Revoke* + - kms:Disable* + - kms:Get* + - kms:Delete* + - kms:ScheduleKeyDeletion + - kms:CancelKeyDeletion + - kms:GenerateDataKey + - kms:TagResource + - kms:UntagResource + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + Resource: "*" + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Principal: + # Not actually everyone -- see below for Conditions + AWS: "*" + Resource: "*" + Condition: + StringEquals: + # See https://docs.aws.amazon.com/kms/latest/developerguide/policy-conditions.html#conditions-kms-caller-account + kms:CallerAccount: + Ref: AWS::AccountId + kms:ViaService: + - Fn::Sub: s3.${AWS::Region}.amazonaws.com + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Principal: + AWS: + Fn::Sub: "${FilePublishingRole.Arn}" + Resource: "*" + Condition: CreateNewKey + FileAssetsBucketEncryptionKeyAlias: + Condition: CreateNewKey + Type: AWS::KMS::Alias + Properties: + AliasName: + Fn::Sub: "alias/cdk-${Qualifier}-assets-key" + TargetKeyId: + Ref: FileAssetsBucketEncryptionKey + StagingBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: + Fn::If: + - HasCustomFileAssetsBucketName + - Fn::Sub: "${FileAssetsBucketName}" + - Fn::Sub: cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region} + AccessControl: Private + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: aws:kms + KMSMasterKeyID: + Fn::If: + - CreateNewKey + - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" + - Fn::If: + - UseAwsManagedKey + - Ref: AWS::NoValue + - Fn::Sub: "${FileAssetsBucketKmsKeyId}" + PublicAccessBlockConfiguration: + Fn::If: + - UsePublicAccessBlockConfiguration + - BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + - Ref: AWS::NoValue + VersioningConfiguration: + Status: Enabled + LifecycleConfiguration: + Rules: + # Exising objects will never be overwritten but Security Hub wants this rule to exist + - Id: CleanupOldVersions + Status: Enabled + NoncurrentVersionExpiration: + NoncurrentDays: 365 + UpdateReplacePolicy: Retain + DeletionPolicy: Retain + 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: '*' + ContainerAssetsRepository: + Type: AWS::ECR::Repository + Properties: + ImageTagMutability: IMMUTABLE + # Untagged images should never exist but Security Hub wants this rule to exist + LifecyclePolicy: + LifecyclePolicyText: | + { + "rules": [ + { + "rulePriority": 1, + "description": "Untagged images should not exist, but expire any older than one year", + "selection": { + "tagStatus": "untagged", + "countType": "sinceImagePushed", + "countUnit": "days", + "countNumber": 365 + }, + "action": { "type": "expire" } + } + ] + } + RepositoryName: + Fn::If: + - HasCustomContainerAssetsRepositoryName + - Fn::Sub: "${ContainerAssetsRepositoryName}" + - Fn::Sub: cdk-${Qualifier}-container-assets-${AWS::AccountId}-${AWS::Region} + RepositoryPolicyText: + Version: "2012-10-17" + Statement: + # Necessary for Lambda container images + # https://docs.aws.amazon.com/lambda/latest/dg/configuration-images.html#configuration-images-permissions + - Sid: LambdaECRImageRetrievalPolicy + Effect: Allow + Principal: { Service: "lambda.amazonaws.com" } + Action: + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Condition: + StringLike: + "aws:sourceArn": { "Fn::Sub": "arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:*" } + FilePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + # The TagSession action is required to be able to assume this role with session tags. + # Without this trust policy, attemping to assume this role with session tags will fail. + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: file-publishing + ImagePublishingRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + # The TagSession action is required to be able to assume this role with session tags. + # Without this trust policy, attemping to assume this role with session tags will fail. + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: image-publishing + LookupRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + # The TagSession action is required to be able to assume this role with session tags. + # Without this trust policy, attemping to assume this role with session tags will fail. + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccountsForLookup + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccountsForLookup + - Ref: AWS::NoValue + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + RoleName: + Fn::Sub: cdk-${Qualifier}-lookup-role-${AWS::AccountId}-${AWS::Region} + ManagedPolicyArns: + - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/ReadOnlyAccess" + Policies: + - PolicyDocument: + Statement: + - Sid: DontReadSecrets + Effect: Deny + Action: + - kms:Decrypt + Resource: "*" + Version: '2012-10-17' + PolicyName: LookupRolePolicy + Tags: + - Key: aws-cdk:bootstrap-role + Value: lookup + FilePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - s3:GetObject* + - s3:GetBucket* + - s3:GetEncryptionConfiguration + - s3:List* + - s3:DeleteObject* + - s3:PutObject* + - s3:Abort* + Resource: + - Fn::Sub: "${StagingBucket.Arn}" + - Fn::Sub: "${StagingBucket.Arn}/*" + Condition: + StringEquals: + aws:ResourceAccount: + - Fn::Sub: ${AWS::AccountId} + Effect: Allow + - Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Effect: Allow + Resource: + Fn::If: + - CreateNewKey + - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" + - Fn::Sub: arn:${AWS::Partition}:kms:${AWS::Region}:${AWS::AccountId}:key/${FileAssetsBucketKmsKeyId} + Version: '2012-10-17' + Roles: + - Ref: FilePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-file-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + ImagePublishingRoleDefaultPolicy: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: + - ecr:PutImage + - ecr:InitiateLayerUpload + - ecr:UploadLayerPart + - ecr:CompleteLayerUpload + - ecr:BatchCheckLayerAvailability + - ecr:DescribeRepositories + - ecr:DescribeImages + - ecr:BatchGetImage + - ecr:GetDownloadUrlForLayer + Resource: + Fn::Sub: "${ContainerAssetsRepository.Arn}" + Effect: Allow + - Action: + - ecr:GetAuthorizationToken + Resource: "*" + Effect: Allow + Version: '2012-10-17' + Roles: + - Ref: ImagePublishingRole + PolicyName: + Fn::Sub: cdk-${Qualifier}-image-publishing-role-default-policy-${AWS::AccountId}-${AWS::Region} + DeploymentActionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + # The TagSession action is required to be able to assume this role with session tags. + # Without this trust policy, attemping to assume this role with session tags will fail. + - Action: sts:TagSession + Effect: Allow + Principal: + AWS: + Ref: AWS::AccountId + - Fn::If: + - HasTrustedAccounts + - Action: sts:AssumeRole + Effect: Allow + Principal: + AWS: + Ref: TrustedAccounts + - Ref: AWS::NoValue + Policies: + - PolicyDocument: + Statement: + - Sid: CloudFormationPermissions + Effect: Allow + Action: + - cloudformation:CreateChangeSet + - cloudformation:DeleteChangeSet + - cloudformation:DescribeChangeSet + - cloudformation:DescribeStacks + - cloudformation:ExecuteChangeSet + - cloudformation:CreateStack + - cloudformation:UpdateStack + Resource: "*" + - Sid: PipelineCrossAccountArtifactsBucket + # Read/write buckets in different accounts. Permissions to buckets in + # same account are granted by bucket policies. + # + # Write permissions necessary to write outputs to the cross-region artifact replication bucket + # https://aws.amazon.com/premiumsupport/knowledge-center/codepipeline-deploy-cloudformation/. + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + - s3:Abort* + - s3:DeleteObject* + - s3:PutObject* + Resource: "*" + Condition: + StringNotEquals: + s3:ResourceAccount: + Ref: 'AWS::AccountId' + - Sid: PipelineCrossAccountArtifactsKey + # Use keys only for the purposes of reading encrypted files from S3. + Effect: Allow + Action: + - kms:Decrypt + - kms:DescribeKey + - kms:Encrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + Resource: "*" + Condition: + StringEquals: + kms:ViaService: + Fn::Sub: s3.${AWS::Region}.amazonaws.com + - Action: iam:PassRole + Resource: + Fn::Sub: "${CloudFormationExecutionRole.Arn}" + Effect: Allow + # Permissions to allow the Deploy Role to perform SQS Actions. + # Users of this bootstrap template intend to uses the Deploy Role + # instead of the CFN ExecutionRole, so the deploy role needs permissions + # to perform CFN actions; in this simple case, we only permit SQS Actions. + - Sid: SQSPermissions + Action: sqs:* + Resource: "*" + Effect: Allow + # This condition requires that the Deploy Role is assumed with the session tags + # 'Department: Engineering'; if these tags are not passed in, the DeployRole will + # not be able to perform SQS actions. + Condition: + StringEquals: + aws:PrincipalTag/Department: "Engineering" + - Sid: CliPermissions + Action: + # Permissions needed by the CLI when doing `cdk deploy`. + # Our CI/CD does not need DeleteStack, + # but we also want to use this role from the CLI, + # and there you can call `cdk destroy` + - cloudformation:DescribeStackEvents + - cloudformation:GetTemplate + - cloudformation:DeleteStack + - cloudformation:UpdateTerminationProtection + - sts:GetCallerIdentity + # `cdk import` + - cloudformation:GetTemplateSummary + Resource: "*" + Effect: Allow + - Sid: CliStagingBucket + Effect: Allow + Action: + - s3:GetObject* + - s3:GetBucket* + - s3:List* + Resource: + - Fn::Sub: ${StagingBucket.Arn} + - Fn::Sub: ${StagingBucket.Arn}/* + - Sid: ReadVersion + Effect: Allow + Action: + - ssm:GetParameter + - ssm:GetParameters # CreateChangeSet uses this to evaluate any SSM parameters (like `CdkBootstrapVersion`) + Resource: + - Fn::Sub: "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter${CdkBootstrapVersion}" + Version: '2012-10-17' + PolicyName: default + RoleName: + Fn::Sub: cdk-${Qualifier}-deploy-role-${AWS::AccountId}-${AWS::Region} + Tags: + - Key: aws-cdk:bootstrap-role + Value: deploy + CloudFormationExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: cloudformation.amazonaws.com + Version: '2012-10-17' + ManagedPolicyArns: + Fn::If: + - HasCloudFormationExecutionPolicies + - Ref: CloudFormationExecutionPolicies + - Fn::If: + - HasTrustedAccounts + # The CLI will prevent this case from occurring + - Ref: AWS::NoValue + # The CLI will advertise that we picked this implicitly + - - Fn::Sub: "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess" + RoleName: + Fn::Sub: cdk-${Qualifier}-cfn-exec-role-${AWS::AccountId}-${AWS::Region} + PermissionsBoundary: + Fn::If: + - PermissionsBoundarySet + - Fn::Sub: 'arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/${InputPermissionsBoundary}' + - Ref: AWS::NoValue + CdkBoostrapPermissionsBoundaryPolicy: + # Edit the template prior to boostrap in order to have this example policy created + Condition: ShouldCreatePermissionsBoundary + Type: AWS::IAM::ManagedPolicy + Properties: + PolicyDocument: + Statement: + # If permission boundaries do not have an explicit `allow`, then the effect is `deny` + - Sid: ExplicitAllowAll + Action: + - "*" + Effect: Allow + Resource: "*" + # Default permissions to prevent privilege escalation + - Sid: DenyAccessIfRequiredPermBoundaryIsNotBeingApplied + Action: + - iam:CreateUser + - iam:CreateRole + - iam:PutRolePermissionsBoundary + - iam:PutUserPermissionsBoundary + Condition: + StringNotEquals: + iam:PermissionsBoundary: + Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + Effect: Deny + Resource: "*" + # Forbid the policy itself being edited + - Sid: DenyPermBoundaryIAMPolicyAlteration + Action: + - iam:CreatePolicyVersion + - iam:DeletePolicy + - iam:DeletePolicyVersion + - iam:SetDefaultPolicyVersion + Effect: Deny + Resource: + Fn::Sub: arn:${AWS::Partition}:iam::${AWS::AccountId}:policy/cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + # Forbid removing the permissions boundary from any user or role that has it associated + - Sid: DenyRemovalOfPermBoundaryFromAnyUserOrRole + Action: + - iam:DeleteUserPermissionsBoundary + - iam:DeleteRolePermissionsBoundary + Effect: Deny + Resource: "*" + # Add your specific organizational security policy here + # Uncomment the example to deny access to AWS Config + #- Sid: OrganizationalSecurityPolicy + # Action: + # - "config:*" + # Effect: Deny + # Resource: "*" + Version: "2012-10-17" + Description: "Bootstrap Permission Boundary" + ManagedPolicyName: + Fn::Sub: cdk-${Qualifier}-permissions-boundary-${AWS::AccountId}-${AWS::Region} + Path: / + # The SSM parameter is used in pipeline-deployed templates to verify the version + # of the bootstrap resources. + CdkBootstrapVersion: + Type: AWS::SSM::Parameter + Properties: + Type: String + Name: + Fn::Sub: '/cdk-bootstrap/${Qualifier}/version' + Value: '22' +Outputs: + BucketName: + Description: The name of the S3 bucket owned by the CDK toolkit stack + Value: + Fn::Sub: "${StagingBucket}" + BucketDomainName: + Description: The domain name of the S3 bucket owned by the CDK toolkit stack + Value: + Fn::Sub: "${StagingBucket.RegionalDomainName}" + # @deprecated - This Export can be removed at some future point in time. + # We can't do it today because if there are stacks that use it, the bootstrap + # stack cannot be updated. Not used anymore by apps >= 1.60.0 + FileAssetKeyArn: + Description: The ARN of the KMS key used to encrypt the asset bucket (deprecated) + Value: + Fn::If: + - CreateNewKey + - Fn::Sub: "${FileAssetsBucketEncryptionKey.Arn}" + - Fn::Sub: "${FileAssetsBucketKmsKeyId}" + Export: + Name: + Fn::Sub: CdkBootstrap-${Qualifier}-FileAssetKeyArn + ImageRepositoryName: + Description: The name of the ECR repository which hosts docker image assets + Value: + Fn::Sub: "${ContainerAssetsRepository}" + # The Output is used by the CLI to verify the version of the bootstrap resources. + BootstrapVersion: + Description: The version of the bootstrap resources that are currently mastered + in this stack + Value: + Fn::GetAtt: [CdkBootstrapVersion, Value] \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js new file mode 100755 index 000000000..269b5cdf3 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js @@ -0,0 +1,971 @@ +const path = require('path'); + +var constructs = require('constructs'); +if (process.env.PACKAGE_LAYOUT_VERSION === '1') { + var cdk = require('@aws-cdk/core'); + var ec2 = require('@aws-cdk/aws-ec2'); + var ecs = require('@aws-cdk/aws-ecs'); + var s3 = require('@aws-cdk/aws-s3'); + var ssm = require('@aws-cdk/aws-ssm'); + var iam = require('@aws-cdk/aws-iam'); + var sns = require('@aws-cdk/aws-sns'); + var sqs = require('@aws-cdk/aws-sqs'); + var lambda = require('@aws-cdk/aws-lambda'); + var node_lambda = require('@aws-cdk/aws-lambda-nodejs'); + var sso = require('@aws-cdk/aws-sso'); + var docker = require('@aws-cdk/aws-ecr-assets'); + var appsync = require('@aws-cdk/aws-appsync'); +} else { + var cdk = require('aws-cdk-lib'); + var { + DefaultStackSynthesizer, + LegacyStackSynthesizer, + aws_ec2: ec2, + aws_ecs: ecs, + aws_sso: sso, + aws_s3: s3, + aws_ssm: ssm, + aws_iam: iam, + aws_sns: sns, + aws_sqs: sqs, + aws_lambda: lambda, + aws_lambda_nodejs: node_lambda, + aws_ecr_assets: docker, + aws_appsync: appsync, + Stack + } = require('aws-cdk-lib'); +} + +const { Annotations } = cdk; +const { StackWithNestedStack, StackWithDoublyNestedStack, StackWithNestedStackUsingParameters } = require('./nested-stack'); + +const stackPrefix = process.env.STACK_NAME_PREFIX; +if (!stackPrefix) { + throw new Error(`the STACK_NAME_PREFIX environment variable is required`); +} + +class MyStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + new sns.Topic(this, 'topic'); + + if (cdk.AvailabilityZoneProvider) { // <= 0.34.0 + new cdk.AvailabilityZoneProvider(this).availabilityZones; + } else if (cdk.Context) { // <= 0.35.0 + cdk.Context.getAvailabilityZones(this); + } else { + this.availabilityZones; + } + + const parameterName = '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'; + getSsmParameterValue(this, parameterName); + } +} + +function getSsmParameterValue(scope, parameterName) { + return ssm.StringParameter.valueFromLookup(scope, parameterName); +} + +class YourStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + new sns.Topic(this, 'topic1'); + new sns.Topic(this, 'topic2'); + } +} + +class NoticesStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + new sqs.Queue(this, 'queue'); + } +} + +class SsoPermissionSetNoPolicy extends Stack { + constructor(scope, id) { + super(scope, id); + + new sso.CfnPermissionSet(this, "permission-set-without-managed-policy", { + instanceArn: 'arn:aws:sso:::instance/testvalue', + name: 'testName', + permissionsBoundary: { customerManagedPolicyReference: { name: 'why', path: '/how/' } }, + }) + } +} + +class SsoPermissionSetManagedPolicy extends Stack { + constructor(scope, id) { + super(scope, id); + new sso.CfnPermissionSet(this, "permission-set-with-managed-policy", { + managedPolicies: ['arn:aws:iam::aws:policy/administratoraccess'], + customerManagedPolicyReferences: [{ name: 'forSSO' }], + permissionsBoundary: { managedPolicyArn: 'arn:aws:iam::aws:policy/AdministratorAccess' }, + instanceArn: 'arn:aws:sso:::instance/testvalue', + name: 'niceWork', + }) + } +} + +class SsoAssignment extends Stack { + constructor(scope, id) { + super(scope, id); + new sso.CfnAssignment(this, "assignment", { + instanceArn: 'arn:aws:sso:::instance/testvalue', + permissionSetArn: 'arn:aws:sso:::testvalue', + principalId: '11111111-2222-3333-4444-test', + principalType: 'USER', + targetId: '111111111111', + targetType: 'AWS_ACCOUNT' + }); + } +} + +class SsoInstanceAccessControlConfig extends Stack { + constructor(scope, id) { + super(scope, id); + new sso.CfnInstanceAccessControlAttributeConfiguration(this, 'instanceAccessControlConfig', { + instanceArn: 'arn:aws:sso:::instance/testvalue', + accessControlAttributes: [ + { key: 'first', value: { source: ['a'] } }, + { key: 'second', value: { source: ['b'] } }, + { key: 'third', value: { source: ['c'] } }, + { key: 'fourth', value: { source: ['d'] } }, + { key: 'fifth', value: { source: ['e'] } }, + { key: 'sixth', value: { source: ['f'] } }, + ] + }) + } +} + +class ListMultipleDependentStack extends Stack { + constructor(scope, id) { + super(scope, id); + + const dependentStack1 = new DependentStack1(this, 'DependentStack1'); + const dependentStack2 = new DependentStack2(this, 'DependentStack2'); + + this.addDependency(dependentStack1); + this.addDependency(dependentStack2); + } +} + +class DependentStack1 extends Stack { + constructor(scope, id) { + super(scope, id); + + } +} + +class DependentStack2 extends Stack { + constructor(scope, id) { + super(scope, id); + + } +} + +class ListStack extends Stack { + constructor(scope, id) { + super(scope, id); + + const dependentStack = new DependentStack(this, 'DependentStack'); + + this.addDependency(dependentStack); + } +} + +class DependentStack extends Stack { + constructor(scope, id) { + super(scope, id); + + const innerDependentStack = new InnerDependentStack(this, 'InnerDependentStack'); + + this.addDependency(innerDependentStack); + } +} + +class InnerDependentStack extends Stack { + constructor(scope, id) { + super(scope, id); + + } +} + +class MigrateStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + if (!process.env.OMIT_TOPIC) { + const queue = new sqs.Queue(this, 'Queue', { + removalPolicy: process.env.ORPHAN_TOPIC ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY, + }); + + new cdk.CfnOutput(this, 'QueueName', { + value: queue.queueName, + }); + + new cdk.CfnOutput(this, 'QueueUrl', { + value: queue.queueUrl, + }); + + new cdk.CfnOutput(this, 'QueueLogicalId', { + value: queue.node.defaultChild.logicalId, + }); + } + if (process.env.SAMPLE_RESOURCES) { + const myTopic = new sns.Topic(this, 'migratetopic1', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + cdk.Tags.of(myTopic).add('tag1', 'value1'); + const myTopic2 = new sns.Topic(this, 'migratetopic2', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + cdk.Tags.of(myTopic2).add('tag2', 'value2'); + const myQueue = new sqs.Queue(this, 'migratequeue1', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + cdk.Tags.of(myQueue).add('tag3', 'value3'); + } + if (process.env.LAMBDA_RESOURCES) { + const myFunction = new lambda.Function(this, 'migratefunction1', { + code: lambda.Code.fromInline('console.log("hello world")'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_18_X, + }); + cdk.Tags.of(myFunction).add('lambda-tag', 'lambda-value'); + + const myFunction2 = new lambda.Function(this, 'migratefunction2', { + code: lambda.Code.fromInline('console.log("hello world2")'), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_18_X, + }); + cdk.Tags.of(myFunction2).add('lambda-tag', 'lambda-value'); + } + } +} + +class ImportableStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + new cdk.CfnWaitConditionHandle(this, 'Handle'); + + if (process.env.INCLUDE_SINGLE_QUEUE === '1') { + const queue = new sqs.Queue(this, 'Queue', { + removalPolicy: (process.env.RETAIN_SINGLE_QUEUE === '1') ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY, + }); + + new cdk.CfnOutput(this, 'QueueName', { + value: queue.queueName, + }); + + new cdk.CfnOutput(this, 'QueueUrl', { + value: queue.queueUrl, + }); + + new cdk.CfnOutput(this, 'QueueLogicalId', { + value: queue.node.defaultChild.logicalId, + }); + } + + if (process.env.INCLUDE_SINGLE_BUCKET === '1') { + const bucket = new s3.Bucket(this, 'test-bucket', { + removalPolicy: (process.env.RETAIN_SINGLE_BUCKET === '1') ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY, + }); + + new cdk.CfnOutput(this, 'BucketLogicalId', { + value: bucket.node.defaultChild.logicalId, + }); + + new cdk.CfnOutput(this, 'BucketName', { + value: bucket.bucketName, + }); + } + + if (process.env.LARGE_TEMPLATE === '1') { + for (let i = 1; i <= 70; i++) { + new sqs.Queue(this, `cdk-import-queue-test${i}`, { + enforceSSL: true, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + } + } + + if (process.env.INCLUDE_NODEJS_FUNCTION_LAMBDA === '1') { + new node_lambda.NodejsFunction( + this, + 'cdk-import-nodejs-lambda-test', + { + bundling: { + minify: true, + sourceMap: false, + sourcesContent: false, + target: 'ES2020', + forceDockerBundling: true, + }, + runtime: lambda.Runtime.NODEJS_18_X, + entry: path.join(__dirname, 'lambda/index.js') + } + ) + } + } +} + +class StackUsingContext extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + new cdk.CfnResource(this, 'Handle', { + type: 'AWS::CloudFormation::WaitConditionHandle' + }); + + new cdk.CfnOutput(this, 'Output', { + value: this.availabilityZones[0], + }); + } +} + +class ParameterStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new sns.Topic(this, 'TopicParameter', { + topicName: new cdk.CfnParameter(this, 'TopicNameParam').valueAsString + }); + } +} + +class OtherParameterStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new sns.Topic(this, 'TopicParameter', { + topicName: new cdk.CfnParameter(this, 'OtherTopicNameParam').valueAsString + }); + } +} + +class MultiParameterStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new sns.Topic(this, 'TopicParameter', { + displayName: new cdk.CfnParameter(this, 'DisplayNameParam').valueAsString + }); + new sns.Topic(this, 'OtherTopicParameter', { + displayName: new cdk.CfnParameter(this, 'OtherDisplayNameParam').valueAsString + }); + } +} + +class OutputsStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const topic = new sns.Topic(this, 'MyOutput', { + topicName: `${cdk.Stack.of(this).stackName}MyTopic` + }); + + new cdk.CfnOutput(this, 'TopicName', { + value: topic.topicName + }) + } +} + +class TwoSnsTopics extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new sns.Topic(this, 'Topic1'); + new sns.Topic(this, 'Topic2'); + + } +} + +class AnotherOutputsStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const topic = new sns.Topic(this, 'MyOtherOutput', { + topicName: `${cdk.Stack.of(this).stackName}MyOtherTopic` + }); + + new cdk.CfnOutput(this, 'TopicName', { + value: topic.topicName + }); + } +} + +class IamStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new iam.Role(this, 'SomeRole', { + assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') + }); + } +} + +class ProvidingStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + this.topic = new sns.Topic(this, 'BogusTopic'); // Some filler + } +} + +class StackWithError extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + this.topic = new sns.Topic(this, 'BogusTopic'); // Some filler + Annotations.of(this).addError('This is an error'); + } +} + +class StageWithError extends cdk.Stage { + constructor(parent, id, props) { + super(parent, id, props); + + new StackWithError(this, 'Stack'); + } +} + +class ConsumingStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new sns.Topic(this, 'BogusTopic'); // Some filler + new cdk.CfnOutput(this, 'IConsumedSomething', { value: props.providingStack.topic.topicArn }); + } +} + +class MissingSSMParameterStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const parameterName = constructs.Node.of(this).tryGetContext('test:ssm-parameter-name'); + if (parameterName) { + const param = getSsmParameterValue(this, parameterName); + new iam.Role(this, 'PhonyRole', { assumedBy: new iam.AccountPrincipal(param) }); + } + } +} + +class LambdaStack extends cdk.Stack { + constructor(parent, id, props) { + // sometimes we need to specify the custom bootstrap bucket to use + // see the 'upgrade legacy bootstrap stack' test + const synthesizer = parent.node.tryGetContext('legacySynth') === 'true' ? + new LegacyStackSynthesizer({ + fileAssetsBucketName: parent.node.tryGetContext('bootstrapBucket'), + }) + : new DefaultStackSynthesizer({ + fileAssetsBucketName: parent.node.tryGetContext('bootstrapBucket'), + }) + super(parent, id, { + ...props, + synthesizer: synthesizer, + }); + + const fn = new lambda.Function(this, 'my-function', { + code: lambda.Code.asset(path.join(__dirname, 'lambda')), + runtime: lambda.Runtime.NODEJS_LATEST, + handler: 'index.handler' + }); + + new cdk.CfnOutput(this, 'FunctionArn', { value: fn.functionArn }); + } +} + +class IamRolesStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + // Environment variabile is used to create a bunch of roles to test + // that large diff templates are uploaded to S3 to create the changeset. + for (let i = 1; i <= Number(process.env.NUMBER_OF_ROLES); i++) { + const role = new iam.Role(this, `Role${i}`, { + assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), + }); + const cfnRole = role.node.defaultChild; + + // For any extra IAM roles created, add a ton of metadata so that the template size is > 50 KiB. + if (i > 1) { + for (let i = 1; i <= 30; i++) { + cfnRole.addMetadata('a'.repeat(1000), 'v'); + } + } + } + } +} + +class SessionTagsStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, { + ...props, + synthesizer: new DefaultStackSynthesizer({ + deployRoleAdditionalOptions: { + Tags: [{ Key: 'Department', Value: 'Engineering' }] + }, + fileAssetPublishingRoleAdditionalOptions: { + Tags: [{ Key: 'Department', Value: 'Engineering' }] + }, + imageAssetPublishingRoleAdditionalOptions: { + Tags: [{ Key: 'Department', Value: 'Engineering' }] + }, + lookupRoleAdditionalOptions: { + Tags: [{ Key: 'Department', Value: 'Engineering' }] + } + }) + }); + + // VPC lookup to test LookupRole + ec2.Vpc.fromLookup(this, 'DefaultVPC', { isDefault: true }); + + // Lambda Function to test AssetPublishingRole + const fn = new lambda.Function(this, 'my-function', { + code: lambda.Code.asset(path.join(__dirname, 'lambda')), + runtime: lambda.Runtime.NODEJS_LATEST, + handler: 'index.handler' + }); + + // DockerImageAsset to test ImageAssetPublishingRole + new docker.DockerImageAsset(this, 'image', { + directory: path.join(__dirname, 'docker') + }); + } +} + +class NoExecutionRoleCustomSynthesizer extends cdk.DefaultStackSynthesizer { + + emitArtifact(session, options) { + super.emitArtifact(session, { + ...options, + cloudFormationExecutionRoleArn: undefined, + }) + } +} + +class SessionTagsWithNoExecutionRoleCustomSynthesizerStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, { + ...props, + synthesizer: new NoExecutionRoleCustomSynthesizer({ + deployRoleAdditionalOptions: { + Tags: [{ Key: 'Department', Value: 'Engineering' }] + }, + }) + }); + + new sqs.Queue(this, 'sessionTagsQueue'); + } +} +class LambdaHotswapStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const fn = new lambda.Function(this, 'my-function', { + code: lambda.Code.asset(path.join(__dirname, 'lambda')), + runtime: lambda.Runtime.NODEJS_LATEST, + handler: 'index.handler', + description: process.env.DYNAMIC_LAMBDA_PROPERTY_VALUE ?? "description", + environment: { + SomeVariable: + process.env.DYNAMIC_LAMBDA_PROPERTY_VALUE ?? "environment", + ImportValueVariable: process.env.USE_IMPORT_VALUE_LAMBDA_PROPERTY + ? cdk.Fn.importValue(TEST_EXPORT_OUTPUT_NAME) + : "no-import", + }, + }); + + new cdk.CfnOutput(this, 'FunctionName', { value: fn.functionName }); + } +} + +class EcsHotswapStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + // define a simple vpc and cluster + const vpc = new ec2.Vpc(this, 'vpc', { + natGateways: 0, + subnetConfiguration: [ + { + cidrMask: 24, + name: 'Public', + subnetType: ec2.SubnetType.PUBLIC, + }, + ], + maxAzs: 1, + }); + const cluster = new ecs.Cluster(this, 'cluster', { + vpc, + }); + + // allow stack to be used to test failed deployments + const image = + process.env.USE_INVALID_ECS_HOTSWAP_IMAGE == 'true' + ? 'nginx:invalidtag' + : 'nginx:alpine'; + + // deploy basic service + const taskDefinition = new ecs.FargateTaskDefinition( + this, + 'task-definition' + ); + taskDefinition.addContainer('nginx', { + image: ecs.ContainerImage.fromRegistry(image), + environment: { + SOME_VARIABLE: process.env.DYNAMIC_ECS_PROPERTY_VALUE ?? 'environment', + }, + healthCheck: { + command: ['CMD-SHELL', 'exit 0'], // fake health check to speed up deployment + interval: cdk.Duration.seconds(5), + }, + }); + const service = new ecs.FargateService(this, 'service', { + cluster, + taskDefinition, + assignPublicIp: true, // required without NAT to pull image + circuitBreaker: { rollback: false }, + desiredCount: 1, + }); + + new cdk.CfnOutput(this, 'ClusterName', { value: cluster.clusterName }); + new cdk.CfnOutput(this, 'ServiceName', { value: service.serviceName }); + } +} + +class DockerStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new docker.DockerImageAsset(this, 'image', { + directory: path.join(__dirname, 'docker') + }); + + // Add at least a single resource (WaitConditionHandle), otherwise this stack will never + // be deployed (and its assets never built) + new cdk.CfnResource(this, 'Handle', { + type: 'AWS::CloudFormation::WaitConditionHandle' + }); + } +} + +class DockerInUseStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + // Use the docker file in a lambda otherwise it will not be referenced in the template + const fn = new lambda.Function(this, 'my-function', { + code: lambda.Code.fromAssetImage(path.join(__dirname, 'docker')), + runtime: lambda.Runtime.FROM_IMAGE, + handler: lambda.Handler.FROM_IMAGE, + }); + } +} + +class DockerStackWithCustomFile extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new docker.DockerImageAsset(this, 'image', { + directory: path.join(__dirname, 'docker'), + file: 'Dockerfile.Custom' + }); + + // Add at least a single resource (WaitConditionHandle), otherwise this stack will never + // be deployed (and its assets never built) + new cdk.CfnResource(this, 'Handle', { + type: 'AWS::CloudFormation::WaitConditionHandle' + }); + } +} + +/** + * A stack that will never succeed deploying (done in a way that CDK cannot detect but CFN will complain about) + */ +class FailedStack extends cdk.Stack { + + constructor(parent, id, props) { + super(parent, id, props); + + // fails on 'Property PolicyDocument cannot be empty'. + new cdk.CfnResource(this, 'EmptyPolicy', { + type: 'AWS::IAM::Policy' + }) + + } + +} + +const VPC_TAG_NAME = 'custom-tag'; +const VPC_TAG_VALUE = `${stackPrefix}-bazinga!`; + +class DefineVpcStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const vpc = new ec2.Vpc(this, 'VPC', { + maxAzs: 1, + }) + cdk.Aspects.of(vpc).add(new cdk.Tag(VPC_TAG_NAME, VPC_TAG_VALUE)); + } +} + +class ImportVpcStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + ec2.Vpc.fromLookup(this, 'DefaultVPC', { isDefault: true }); + ec2.Vpc.fromLookup(this, 'ByTag', { tags: { [VPC_TAG_NAME]: VPC_TAG_VALUE } }); + } +} + +class ConditionalResourceStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + if (!process.env.NO_RESOURCE) { + new iam.User(this, 'User'); + } + } +} + +const TEST_EXPORT_OUTPUT_NAME = 'test-export-output'; + +class ExportValueStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + // just need any resource to exist within the stack + const topic = new sns.Topic(this, 'Topic'); + + new cdk.CfnOutput(this, 'ExportValueOutput', { + exportName: TEST_EXPORT_OUTPUT_NAME, + value: topic.topicArn, + }); + } +} + +class BundlingStage extends cdk.Stage { + constructor(parent, id, props) { + super(parent, id, props); + const stack = new cdk.Stack(this, 'BundlingStack'); + + new lambda.Function(stack, 'Handler', { + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda')), + handler: 'index.handler', + runtime: lambda.Runtime.NODEJS_LATEST, + }); + } +} + +class SomeStage extends cdk.Stage { + constructor(parent, id, props) { + super(parent, id, props); + + new YourStack(this, 'StackInStage'); + } +} + +class StageUsingContext extends cdk.Stage { + constructor(parent, id, props) { + super(parent, id, props); + + new StackUsingContext(this, 'StackInStage'); + } +} + +class BuiltinLambdaStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new s3.Bucket(this, 'Bucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, // will deploy a Nodejs lambda backed custom resource + }); + } +} + +class NotificationArnsStack extends cdk.Stack { + constructor(parent, id, props) { + + const arnsFromEnv = process.env.INTEG_NOTIFICATION_ARNS; + super(parent, id, { + ...props, + // comma separated list of arns. + // empty string means empty list. + // undefined means undefined + notificationArns: arnsFromEnv == '' ? [] : (arnsFromEnv ? arnsFromEnv.split(',') : undefined) + }); + + new cdk.CfnWaitConditionHandle(this, 'WaitConditionHandle'); + + } +} + +class AppSyncHotswapStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const api = new appsync.GraphqlApi(this, "Api", { + name: "appsync-hotswap", + definition: appsync.Definition.fromFile(path.join(__dirname, 'appsync.hotswap.graphql')), + authorizationConfig: { + defaultAuthorization: { + authorizationType: appsync.AuthorizationType.IAM, + }, + }, + }); + + const noneDataSource = api.addNoneDataSource("none"); + // create 50 appsync functions to hotswap + for (const i of Array(50).keys()) { + const appsyncFunction = new appsync.AppsyncFunction(this, `Function${i}`, { + name: `appsync_function${i}`, + api, + dataSource: noneDataSource, + requestMappingTemplate: appsync.MappingTemplate.fromString(process.env.DYNAMIC_APPSYNC_PROPERTY_VALUE ?? "$util.toJson({})"), + responseMappingTemplate: appsync.MappingTemplate.fromString('$util.toJson({})'), + }); + } + } +} + +class MetadataStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + const handle = new cdk.CfnWaitConditionHandle(this, 'WaitConditionHandle'); + handle.addMetadata('Key', process.env.INTEG_METADATA_VALUE ?? 'default') + + } +} + +const app = new cdk.App({ + context: { + '@aws-cdk/core:assetHashSalt': process.env.CODEBUILD_BUILD_ID ?? process.env.GITHUB_RUN_ID, // Force all assets to be unique, but consistent in one build + }, +}); + +const defaultEnv = { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION +}; + +// Sometimes we don't want to synthesize all stacks because it will impact the results +const stackSet = process.env.INTEG_STACK_SET || 'default'; + +switch (stackSet) { + case 'default': + // Deploy all does a wildcard ${stackPrefix}-test-* + new TwoSnsTopics(app, `${stackPrefix}-two-sns-topics`); + new MyStack(app, `${stackPrefix}-test-1`, { env: defaultEnv }); + new YourStack(app, `${stackPrefix}-test-2`); + new NoticesStack(app, `${stackPrefix}-notices`); + // Deploy wildcard with parameters does ${stackPrefix}-param-test-* + new ParameterStack(app, `${stackPrefix}-param-test-1`); + new OtherParameterStack(app, `${stackPrefix}-param-test-2`); + // Deploy stack with multiple parameters + new MultiParameterStack(app, `${stackPrefix}-param-test-3`); + // Deploy stack with outputs does ${stackPrefix}-outputs-test-* + new OutputsStack(app, `${stackPrefix}-outputs-test-1`); + new AnotherOutputsStack(app, `${stackPrefix}-outputs-test-2`); + // Not included in wildcard + new IamStack(app, `${stackPrefix}-iam-test`, { env: defaultEnv }); + const providing = new ProvidingStack(app, `${stackPrefix}-order-providing`); + new ConsumingStack(app, `${stackPrefix}-order-consuming`, { providingStack: providing }); + + new MissingSSMParameterStack(app, `${stackPrefix}-missing-ssm-parameter`, { env: defaultEnv }); + + new LambdaStack(app, `${stackPrefix}-lambda`); + + // This stack is used to test diff with large templates by creating a role with a ton of metadata + new IamRolesStack(app, `${stackPrefix}-iam-roles`); + + if (process.env.ENABLE_VPC_TESTING == 'IMPORT') { + // this stack performs a VPC lookup so we gate synth + const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }; + new SessionTagsStack(app, `${stackPrefix}-session-tags`, { env }); + } + + new SessionTagsWithNoExecutionRoleCustomSynthesizerStack(app, `${stackPrefix}-session-tags-with-custom-synthesizer`); + new LambdaHotswapStack(app, `${stackPrefix}-lambda-hotswap`); + new EcsHotswapStack(app, `${stackPrefix}-ecs-hotswap`); + new AppSyncHotswapStack(app, `${stackPrefix}-appsync-hotswap`); + new DockerStack(app, `${stackPrefix}-docker`); + new DockerInUseStack(app, `${stackPrefix}-docker-in-use`); + new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`); + + new NotificationArnsStack(app, `${stackPrefix}-notification-arns`); + + // SSO stacks + new SsoInstanceAccessControlConfig(app, `${stackPrefix}-sso-access-control`); + new SsoAssignment(app, `${stackPrefix}-sso-assignment`); + new SsoPermissionSetManagedPolicy(app, `${stackPrefix}-sso-perm-set-with-managed-policy`); + new SsoPermissionSetNoPolicy(app, `${stackPrefix}-sso-perm-set-without-managed-policy`); + + const failed = new FailedStack(app, `${stackPrefix}-failed`) + + // A stack that depends on the failed stack -- used to test that '-e' does not deploy the failing stack + const dependsOnFailed = new OutputsStack(app, `${stackPrefix}-depends-on-failed`); + dependsOnFailed.addDependency(failed); + + if (process.env.ENABLE_VPC_TESTING) { // Gating so we don't do context fetching unless that's what we are here for + const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }; + if (process.env.ENABLE_VPC_TESTING === 'DEFINE') + new DefineVpcStack(app, `${stackPrefix}-define-vpc`, { env }); + if (process.env.ENABLE_VPC_TESTING === 'IMPORT') + new ImportVpcStack(app, `${stackPrefix}-import-vpc`, { env }); + } + + new ConditionalResourceStack(app, `${stackPrefix}-conditional-resource`) + + new StackWithNestedStack(app, `${stackPrefix}-with-nested-stack`); + new StackWithNestedStackUsingParameters(app, `${stackPrefix}-with-nested-stack-using-parameters`); + new StackWithDoublyNestedStack(app, `${stackPrefix}-with-doubly-nested-stack`); + new ListStack(app, `${stackPrefix}-list-stacks`) + new ListMultipleDependentStack(app, `${stackPrefix}-list-multiple-dependent-stacks`); + + new YourStack(app, `${stackPrefix}-termination-protection`, { + terminationProtection: process.env.TERMINATION_PROTECTION !== 'FALSE' ? true : false, + }); + + new SomeStage(app, `${stackPrefix}-stage`); + + new BuiltinLambdaStack(app, `${stackPrefix}-builtin-lambda-function`); + + new ImportableStack(app, `${stackPrefix}-importable-stack`); + + new MigrateStack(app, `${stackPrefix}-migrate-stack`); + + new ExportValueStack(app, `${stackPrefix}-export-value-stack`); + + new BundlingStage(app, `${stackPrefix}-bundling-stage`); + + new MetadataStack(app, `${stackPrefix}-metadata`); + break; + + case 'stage-using-context': + // Cannot be combined with other test stacks, because we use this to test + // that stage context is propagated up and causes synth to fail when combined + // with '--no-lookups'. + + // Needs a dummy stack at the top level because the CLI will fail otherwise + new YourStack(app, `${stackPrefix}-toplevel`, { env: defaultEnv }); + new StageUsingContext(app, `${stackPrefix}-stage-using-context`, { + env: defaultEnv, + }); + break; + + case 'stage-with-errors': + const stage = new StageWithError(app, `${stackPrefix}-stage-with-errors`); + stage.synth({ validateOnSynthesis: true }); + break; + + case 'stage-with-no-stacks': + break; + + default: + throw new Error(`Unrecognized INTEG_STACK_SET: '${stackSet}'`); +} + +app.synth(); diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/appsync.hotswap.graphql b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/appsync.hotswap.graphql new file mode 100644 index 000000000..808e34214 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/appsync.hotswap.graphql @@ -0,0 +1,3 @@ +type Query { + listString: [String] +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/cdk.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/cdk.json new file mode 100644 index 000000000..44809158d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/cdk.json @@ -0,0 +1,7 @@ +{ + "app": "node app.js", + "versionReporting": false, + "context": { + "aws-cdk:enableDiffNoFail": "true" + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/docker/Dockerfile b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/docker/Dockerfile new file mode 100644 index 000000000..a7e84d32f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/docker/Dockerfile @@ -0,0 +1,2 @@ +FROM public.ecr.aws/docker/library/alpine:latest + diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/docker/Dockerfile.Custom b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/docker/Dockerfile.Custom new file mode 100644 index 000000000..a7e84d32f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/docker/Dockerfile.Custom @@ -0,0 +1,2 @@ +FROM public.ecr.aws/docker/library/alpine:latest + diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/lambda/index.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/lambda/index.js new file mode 100644 index 000000000..14f22d487 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/lambda/index.js @@ -0,0 +1,4 @@ +exports.handler = async function(event) { + const response = require('./response.json'); + return response; +}; \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/lambda/response.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/lambda/response.json new file mode 100644 index 000000000..b725f16eb --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/lambda/response.json @@ -0,0 +1,3 @@ +{ + "response": "hello, dear asset!" +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/nested-stack.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/nested-stack.js new file mode 100644 index 000000000..ca3bd19be --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/nested-stack.js @@ -0,0 +1,65 @@ +if (process.env.PACKAGE_LAYOUT_VERSION === '1') { + var cfn = require('@aws-cdk/aws-cloudformation'); + var sns = require('@aws-cdk/aws-sns'); + var { Stack, CfnParameter } = require('@aws-cdk/core'); +} else { + var { + aws_cloudformation: cfn, + aws_sns: sns, + } = require('aws-cdk-lib'); + var { Stack, CfnParameter } = require('aws-cdk-lib'); +} + +class StackWithNestedStack extends Stack { + constructor(scope, id) { + super(scope, id); + new MyNestedStack(this, 'MyNested'); + } +} + +class MyNestedStack extends cfn.NestedStack { + constructor(scope, id) { + super(scope, id); + + new sns.Topic(this, 'MyTopic'); + } +} + +class DoublyNestedStack extends cfn.NestedStack { + constructor(scope, id) { + super(scope, id); + + new MyNestedStack(this, 'Nestor'); + } +} + +class StackWithDoublyNestedStack extends Stack { + constructor(scope, id) { + super(scope, id); + new DoublyNestedStack(this, 'DoubleDouble'); + } +} + +class StackWithNestedStackUsingParameters extends Stack { + constructor(scope, id) { + super(scope, id); + const topicNameParam = new CfnParameter(this, 'MyTopicParam'); + new MyNestedStackUsingParameters(this, 'MyNested', { + parameters: {'MyTopicParam': topicNameParam.valueAsString} + }); + } +} + +class MyNestedStackUsingParameters extends cfn.NestedStack { + constructor(scope, id, props) { + super(scope, id, props); + + new sns.Topic(this, 'MyTopic', { + topicName: new CfnParameter(this, 'MyTopicParam') + }); + } +} + +exports.StackWithNestedStack = StackWithNestedStack; +exports.StackWithNestedStackUsingParameters = StackWithNestedStackUsingParameters; +exports.StackWithDoublyNestedStack = StackWithDoublyNestedStack; diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/.gitignore b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/.gitignore new file mode 100644 index 000000000..71d72642a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/.gitignore @@ -0,0 +1 @@ +!cfn-include-app.js diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/cdk.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/cdk.json new file mode 100644 index 000000000..e5077eaae --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/cdk.json @@ -0,0 +1,4 @@ +{ + "app": "node cfn-include-app.js", + "versionReporting": false +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/cfn-include-app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/cfn-include-app.js new file mode 100644 index 000000000..6c5ddc776 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/cfn-include-app.js @@ -0,0 +1,21 @@ +const path = require('path'); + +const uberPackage = process.env.UBERPACKAGE; +if (!uberPackage) { + throw new Error('The UBERPACKAGE environment variable is required for running this app!'); +} + +const cfn_inc = require(`${uberPackage}/cloudformation-include`); +const core = require(`${uberPackage}`); + +const app = new core.App(); +const stack = new core.Stack(app, 'Stack'); +const cfnInclude = new cfn_inc.CfnInclude(stack, 'Template', { + templateFile: path.join(__dirname, 'example-template.json'), +}); +const cfnBucket = cfnInclude.getResource('Bucket'); +if (cfnBucket.bucketName !== 'my-example-bucket') { + throw new Error(`Expected bucketName to be 'my-example-bucket', got: '${cfnBucket.bucketName}'`); +} + +app.synth(); diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/example-template.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/example-template.json new file mode 100644 index 000000000..8ad9310b8 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/cfn-include-app/example-template.json @@ -0,0 +1,13 @@ +{ + "Resources": { + "NoopHandle": { + "Type": "AWS::CloudFormation::WaitConditionHandle" + }, + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": "my-example-bucket" + } + } + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/rollback-test-app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/rollback-test-app/app.js new file mode 100644 index 000000000..413bcbd19 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/rollback-test-app/app.js @@ -0,0 +1,110 @@ +const cdk = require('aws-cdk-lib'); +const lambda = require('aws-cdk-lib/aws-lambda'); +const sqs = require('aws-cdk-lib/aws-sqs'); +const cr = require('aws-cdk-lib/custom-resources'); + +/** + * This stack will be deployed in multiple phases, to achieve a very specific effect + * + * It contains resources r1 and r2, and a queue q, where r1 gets deployed first. + * + * - PHASE = 1: both resources deploy regularly. + * - PHASE = 2a: r1 gets updated, r2 will fail to update + * - PHASE = 2b: r1 gets updated, r2 will fail to update, and r1 will fail its rollback. + * - PHASE = 3: q gets replaced w.r.t. phases 1 and 2 + * + * To exercise this app: + * + * ``` + * env PHASE=1 npx cdk deploy + * env PHASE=2b npx cdk deploy --no-rollback + * # This will leave the stack in UPDATE_FAILED + * + * env PHASE=2b npx cdk rollback + * # This will start a rollback that will fail because r1 fails its rollabck + * + * env PHASE=2b npx cdk rollback --force + * # This will retry the rollback and skip r1 + * ``` + */ +class RollbacktestStack extends cdk.Stack { + constructor(scope, id, props) { + super(scope, id, props); + + let r1props = {}; + let r2props = {}; + let fifo = false; + + const phase = process.env.PHASE; + switch (phase) { + case '1': + // Normal deployment + break; + case '2a': + // r1 updates normally, r2 fails updating + r2props.FailUpdate = true; + break; + case '2b': + // r1 updates normally, r2 fails updating, r1 fails rollback + r1props.FailRollback = true; + r2props.FailUpdate = true; + break; + case '3': + fifo = true; + break; + } + + const fn = new lambda.Function(this, 'Fun', { + runtime: lambda.Runtime.NODEJS_LATEST, + code: lambda.Code.fromInline(`exports.handler = async function(event, ctx) { + const key = \`Fail\${event.RequestType}\`; + if (event.ResourceProperties[key]) { + throw new Error(\`\${event.RequestType} fails!\`); + } + if (event.OldResourceProperties?.FailRollback) { + throw new Error('Failing rollback!'); + } + return {}; + }`), + handler: 'index.handler', + timeout: cdk.Duration.minutes(1), + }); + const provider = new cr.Provider(this, "MyProvider", { + onEventHandler: fn, + }); + + const r1 = new cdk.CustomResource(this, 'r1', { + serviceToken: provider.serviceToken, + properties: r1props, + }); + const r2 = new cdk.CustomResource(this, 'r2', { + serviceToken: provider.serviceToken, + properties: r2props, + }); + r2.node.addDependency(r1); + + new sqs.Queue(this, 'Queue', { + fifo, + }); + } +} + +const app = new cdk.App({ + context: { + '@aws-cdk/core:assetHashSalt': process.env.CODEBUILD_BUILD_ID ?? process.env.GITHUB_RUN_ID, // Force all assets to be unique, but consistent in one build + }, +}); + +const defaultEnv = { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION +}; + +const stackPrefix = process.env.STACK_NAME_PREFIX; +if (!stackPrefix) { + throw new Error(`the STACK_NAME_PREFIX environment variable is required`); +} + +// Sometimes we don't want to synthesize all stacks because it will impact the results +new RollbacktestStack(app, `${stackPrefix}-test-rollback`, { env: defaultEnv }); +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/rollback-test-app/cdk.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/rollback-test-app/cdk.json new file mode 100644 index 000000000..44809158d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/rollback-test-app/cdk.json @@ -0,0 +1,7 @@ +{ + "app": "node app.js", + "versionReporting": false, + "context": { + "aws-cdk:enableDiffNoFail": "true" + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/bin/test-app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/bin/test-app.js new file mode 100644 index 000000000..c694edf82 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/bin/test-app.js @@ -0,0 +1,11 @@ +#!/usr/bin/env node +if (process.env.PACKAGE_LAYOUT_VERSION === '1') { + var { App } = require('@aws-cdk/core'); +} else { + var { App } = require('aws-cdk-lib'); +} +var test_stack_1 = require('../lib/test-stack'); +var app = new App(); +const stackPrefix = process.env.STACK_NAME_PREFIX; +new test_stack_1.CDKSupportDemoRootStack(app, `${stackPrefix}-TestStack`); +app.synth(); diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/cdk.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/cdk.json new file mode 100644 index 000000000..3c01626b7 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/cdk.json @@ -0,0 +1,6 @@ +{ + "app": "node bin/test-app.js", + "context": { + + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/lib/nested-stack.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/lib/nested-stack.js new file mode 100644 index 000000000..8620fb693 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/lib/nested-stack.js @@ -0,0 +1,19 @@ +if (process.env.PACKAGE_LAYOUT_VERSION === '1') { + var { NestedStack } = require('@aws-cdk/core'); + var { Function, Runtime, Code } = require('@aws-cdk/aws-lambda'); +} else { + var { NestedStack } = require('aws-cdk-lib'); + var { Function, Runtime, Code } = require('aws-cdk-lib/aws-lambda'); +} + +class NestedStack1 extends NestedStack { + constructor(scope, id, props) { + super(scope, id, props); + new Function(this, 'FunctionPythonRuntime', { + runtime: Runtime.PYTHON_3_12, + code: Code.fromAsset('./src/python/Function'), + handler: 'app.lambda_handler', + }); + } +} +exports.NestedStack1 = NestedStack1; diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/lib/test-stack.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/lib/test-stack.js new file mode 100644 index 000000000..935edcf15 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/lib/test-stack.js @@ -0,0 +1,136 @@ +var path = require('path'); +var { NestedStack1 } = require('./nested-stack'); +if (process.env.PACKAGE_LAYOUT_VERSION === '1') { + var { Stack } = require('@aws-cdk/core'); + var { Runtime, LayerVersion, Code, Function, DockerImageFunction, DockerImageCode } = require('@aws-cdk/aws-lambda'); + var { SpecRestApi, ApiDefinition } = require('@aws-cdk/aws-apigateway'); + var { NodejsFunction } = require('@aws-cdk/aws-lambda-nodejs'); + var { GoFunction } = require('@aws-cdk/aws-lambda-go'); + var { PythonFunction, PythonLayerVersion } = require('@aws-cdk/aws-lambda-python'); + var { Role, ServicePrincipal, PolicyStatement } = require('@aws-cdk/aws-iam'); + var { RetentionDays } = require('@aws-cdk/aws-logs'); +} else { + var { Stack } = require('aws-cdk-lib'); + var { Runtime, LayerVersion, Code, Function, DockerImageFunction, DockerImageCode } = require('aws-cdk-lib/aws-lambda'); + var { SpecRestApi, ApiDefinition } = require('aws-cdk-lib/aws-apigateway'); + var { NodejsFunction } = require('aws-cdk-lib/aws-lambda-nodejs'); + var { GoFunction } = require('@aws-cdk/aws-lambda-go-alpha'); + var { PythonFunction, PythonLayerVersion } = require('@aws-cdk/aws-lambda-python-alpha'); + var { Role, ServicePrincipal, PolicyStatement } = require('aws-cdk-lib/aws-iam'); + var { RetentionDays } = require('aws-cdk-lib/aws-logs'); +} + +const isRunningOnCodeBuild = !!process.env.CODEBUILD_BUILD_ID; +const isRunningOnGitHubActions = !!process.env.GITHUB_RUN_ID; +const isRunningOnCi = isRunningOnCodeBuild || isRunningOnGitHubActions; + +class CDKSupportDemoRootStack extends Stack{ + constructor(scope, id, props) { + super(scope, id, props); + // Python Runtime + // Layers + new PythonLayerVersion(this, 'PythonLayerVersion', { + compatibleRuntimes: [ + Runtime.PYTHON_3_12, + ], + entry: './src/python/Layer', + }); + new LayerVersion(this, 'LayerVersion', { + compatibleRuntimes: [ + Runtime.PYTHON_3_12, + ], + code: Code.fromAsset('./src/python/Layer'), + }); + new LayerVersion(this, 'BundledLayerVersionPythonRuntime', { + compatibleRuntimes: [ + Runtime.PYTHON_3_12, + ], + code: Code.fromAsset('./src/python/Layer', { + bundling: { + command: [ + '/bin/sh', + '-c', + 'rm -rf /tmp/asset-input && mkdir /tmp/asset-input && cp * /tmp/asset-input && cd /tmp/asset-input && pip install -r requirements.txt -t . && mkdir /asset-output/python && cp -R /tmp/asset-input/* /asset-output/python', + ], + image: Runtime.PYTHON_3_12.bundlingImage, + user: 'root', + } + }), + }); + + // ZIP package type Functions + new PythonFunction(this, 'PythonFunction', { + entry: './src/python/Function', + index: 'app.py', + handler: 'lambda_handler', + runtime: Runtime.PYTHON_3_12, + functionName: 'pythonFunc', + logRetention: RetentionDays.THREE_MONTHS, + }); + new Function(this, 'FunctionPythonRuntime', { + runtime: Runtime.PYTHON_3_12, + code: Code.fromAsset('./src/python/Function'), + handler: 'app.lambda_handler', + }); + new Function(this, 'BundledFunctionPythonRuntime', { + runtime: Runtime.PYTHON_3_12, + code: Code.fromAsset('./src/python/Function/', { + bundling: { + command: [ + '/bin/sh', + '-c', + 'rm -rf /tmp/asset-input && mkdir /tmp/asset-input && cp * /tmp/asset-input && cd /tmp/asset-input && pip install -r requirements.txt -t . && cp -R /tmp/asset-input/* /asset-output', + ], + image: Runtime.PYTHON_3_12.bundlingImage, + user: 'root', + } + }), + handler: 'app.lambda_handler', + }); + + // NodeJs Runtime + new NodejsFunction(this, 'NodejsFunction', { + entry: path.join(__dirname, '../src/nodejs/NodeJsFunctionConstruct/app.ts'), + depsLockFilePath: path.join(__dirname, '../src/nodejs/NodeJsFunctionConstruct/package-lock.json'), + bundling: { + forceDockerBundling: true, + }, + handler: 'lambdaHandler', + }); + + // Go Runtime + new GoFunction(this, 'GoFunction', { + entry: './src/go/GoFunctionConstruct', + bundling: { + forcedDockerBundling: true, + // Only use Google proxy in the CI tests, as it is blocked on workstations + goProxies: isRunningOnCi ? [GoFunction.GOOGLE_GOPROXY, 'direct'] : undefined, + }, + }); + + // Image Package Type Functions + new DockerImageFunction(this, 'DockerImageFunction', { + code: DockerImageCode.fromImageAsset('./src/docker/DockerImageFunctionConstruct', { + file: 'Dockerfile', + }), + }); + + // Spec Rest Api + new SpecRestApi(this, 'SpecRestAPI', { + apiDefinition: ApiDefinition.fromAsset('./src/rest-api-definition.yaml'), + }); + // Role to be used as credentials for the Spec rest APi + // it is used inside the spec rest api definition file + new Role(this, 'SpecRestApiRole', { + assumedBy: new ServicePrincipal('apigateway.amazonaws.com'), + roleName: 'SpecRestApiRole', + }).addToPolicy(new PolicyStatement({ + actions: ['lambda:InvokeFunction'], + resources: ['*'], + })); + + // Nested Stack + new NestedStack1(this, 'NestedStack', {}); + } +} +exports.CDKSupportDemoRootStack = CDKSupportDemoRootStack; diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/docker/DockerImageFunctionConstruct/.no-packagejson-validator b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/docker/DockerImageFunctionConstruct/.no-packagejson-validator new file mode 100644 index 000000000..e69de29bb diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/docker/DockerImageFunctionConstruct/Dockerfile b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/docker/DockerImageFunctionConstruct/Dockerfile new file mode 100644 index 000000000..52bc1cd00 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/docker/DockerImageFunctionConstruct/Dockerfile @@ -0,0 +1,11 @@ +FROM public.ecr.aws/lambda/nodejs:18 + +# Assumes your function is named "app.js", and there is a package.json file in the app directory +COPY app.js package.json ./ + +RUN npm install + +USER nobody + +# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile) +CMD [ "app.lambdaHandler" ] \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/docker/DockerImageFunctionConstruct/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/docker/DockerImageFunctionConstruct/app.js new file mode 100644 index 000000000..c67f24bcd --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/docker/DockerImageFunctionConstruct/app.js @@ -0,0 +1,22 @@ +var gen = require('unique-names-generator'); + +const colorName = gen.uniqueNamesGenerator({ + dictionaries: [gen.colors] +}); + +exports.lambdaHandler = async(event, context) => { + let response; + + try { + response = { + 'statusCode': 200, + 'body': JSON.stringify({ + message: "Hello World", + }) + }; + } catch (err) { + console.log(err); + return err; + } + return response; +}; diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/docker/DockerImageFunctionConstruct/package.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/docker/DockerImageFunctionConstruct/package.json new file mode 100644 index 000000000..09669ff5f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/docker/DockerImageFunctionConstruct/package.json @@ -0,0 +1,18 @@ +{ + "name": "DockerImageFunctionConstruct", + "description": "testing docker lambda function ", + "version": "0.1.0", + "bin": { + "randomColors": "bin/app.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "license": "Apache-2.0", + "dependencies": { + "unique-names-generator": "^4.6.0" + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/go/GoFunctionConstruct/go.mod b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/go/GoFunctionConstruct/go.mod new file mode 100644 index 000000000..ac54bd0c3 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/go/GoFunctionConstruct/go.mod @@ -0,0 +1,5 @@ +require github.com/aws/aws-lambda-go v1.28.0 + +module hello-world + +go 1.15 diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/go/GoFunctionConstruct/go.sum b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/go/GoFunctionConstruct/go.sum new file mode 100644 index 000000000..4c65c61e8 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/go/GoFunctionConstruct/go.sum @@ -0,0 +1,17 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aws/aws-lambda-go v1.28.0 h1:fZiik1PZqW2IyAN4rj+Y0UBaO1IDFlsNo9Zz/XnArK4= +github.com/aws/aws-lambda-go v1.28.0/go.mod h1:jJmlefzPfGnckuHdXX7/80O3BvUUi12XOkbv4w9SGLU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/go/GoFunctionConstruct/main.go b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/go/GoFunctionConstruct/main.go new file mode 100644 index 000000000..c2bb7019c --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/go/GoFunctionConstruct/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-lambda-go/lambda" +) + +func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { + return events.APIGatewayProxyResponse{ + Body: "{\"message\": \"Hello World\"}", + StatusCode: 200, + }, nil +} + +func main() { + lambda.Start(handler) +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/nodejs/NodeJsFunctionConstruct/.no-packagejson-validator b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/nodejs/NodeJsFunctionConstruct/.no-packagejson-validator new file mode 100644 index 000000000..e69de29bb diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/nodejs/NodeJsFunctionConstruct/app.ts b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/nodejs/NodeJsFunctionConstruct/app.ts new file mode 100644 index 000000000..131e0a5e3 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/nodejs/NodeJsFunctionConstruct/app.ts @@ -0,0 +1,16 @@ +let response; + +exports.lambdaHandler = async(event, context) => { + try { + response = { + 'statusCode': 200, + 'body': JSON.stringify({ + message: `Hello World`, + }) + }; + } catch (err) { + console.log(err); + return err; + } + return response; +}; diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/nodejs/NodeJsFunctionConstruct/package-lock.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/nodejs/NodeJsFunctionConstruct/package-lock.json new file mode 100644 index 000000000..26a44e6e9 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/nodejs/NodeJsFunctionConstruct/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "hello_world", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "hello_world", + "version": "1.0.0" + } + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/nodejs/NodeJsFunctionConstruct/package.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/nodejs/NodeJsFunctionConstruct/package.json new file mode 100644 index 000000000..447fdeb17 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/nodejs/NodeJsFunctionConstruct/package.json @@ -0,0 +1,5 @@ +{ + "name": "hello_world", + "version": "1.0.0", + "main": "app.js" +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Function/app.py b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Function/app.py new file mode 100644 index 000000000..d06096733 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Function/app.py @@ -0,0 +1,15 @@ +from geonamescache import GeonamesCache +import json + + +def lambda_handler(event, context): + + response = { + "statusCode": 200, + "body": json.dumps( + { + "message": "Hello World", + } + ), + } + return response diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Function/requirements.txt b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Function/requirements.txt new file mode 100644 index 000000000..23a7f0869 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Function/requirements.txt @@ -0,0 +1 @@ +geonamescache==1.3.0 \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Layer/layer_version_dependency.py b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Layer/layer_version_dependency.py new file mode 100644 index 000000000..e0d6427f8 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Layer/layer_version_dependency.py @@ -0,0 +1,5 @@ +from geonamescache import GeonamesCache + + +def get_dependency(): + return 5 diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Layer/requirements.txt b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Layer/requirements.txt new file mode 100644 index 000000000..5071aed63 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/python/Layer/requirements.txt @@ -0,0 +1 @@ +geonamescache==1.3.0 diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/rest-api-definition.yaml b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/rest-api-definition.yaml new file mode 100644 index 000000000..517eaaaea --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/sam_cdk_integ_app/src/rest-api-definition.yaml @@ -0,0 +1,12 @@ +openapi: '3.0.2' +info: + title: API Gateway IP Filtering Example API + +paths: + "/restapis/spec/pythonFunction": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: AWS_PROXY + uri: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:function:pythonFunc:$LATEST/invocations + credentials: arn:${AWS::Partition}:iam::${AWS::AccountId}:role/SpecRestApiRole \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/simple-app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/simple-app/app.js new file mode 100755 index 000000000..f709fbbb4 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/simple-app/app.js @@ -0,0 +1,26 @@ +const cdk = require('aws-cdk-lib/core'); +const iam = require('aws-cdk-lib/aws-iam'); +const sqs = require('aws-cdk-lib/aws-sqs'); + +const stackPrefix = process.env.STACK_NAME_PREFIX; +if (!stackPrefix) { + throw new Error(`the STACK_NAME_PREFIX environment variable is required`); +} + +class SimpleStack extends cdk.Stack { + constructor(scope, id, props) { + super(scope, id, props); + const queue = new sqs.Queue(this, 'queue', { + visibilityTimeout: cdk.Duration.seconds(300), + }); + const role = new iam.Role(this, 'role', { + assumedBy: new iam.AccountRootPrincipal(), + }); + queue.grantConsumeMessages(role); + } +} + +const app = new cdk.App(); +new SimpleStack(app, `${stackPrefix}-simple-1`); + +app.synth(); diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/simple-app/cdk.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/simple-app/cdk.json new file mode 100644 index 000000000..44809158d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/simple-app/cdk.json @@ -0,0 +1,7 @@ +{ + "app": "node app.js", + "versionReporting": false, + "context": { + "aws-cdk:enableDiffNoFail": "true" + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.119.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.119.0/NOTES.md new file mode 100644 index 000000000..5ca96b632 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.119.0/NOTES.md @@ -0,0 +1,5 @@ +This [PR](https://github.com/aws/aws-cdk/pull/16205) added a node version check to our CLI courtesy of [`@jsii/check-node/run`](https://github.com/aws/jsii/tree/main/packages/%40jsii/check-node). + +This check now causes the CLI to print a deprecation warning that changes the output of the `synth` command. We don't consider this a breaking change since we have no guarantess for CLI output, but it did break some our integ tests (namely `cdk synth`) that used to rely on a specific output. + +This patch brings the [fix](https://github.com/aws/aws-cdk/pull/16216) into the regression suite. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.119.0/cli.integtest.js b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.119.0/cli.integtest.js new file mode 100644 index 000000000..b8009dcaa --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.119.0/cli.integtest.js @@ -0,0 +1,659 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = require("fs"); +const os = require("os"); +const path = require("path"); +const aws_1 = require("../helpers/aws"); +const cdk_1 = require("../helpers/cdk"); +const test_helpers_1 = require("../helpers/test-helpers"); +jest.setTimeout(600 * 1000); +test_helpers_1.integTest('VPC Lookup', cdk_1.withDefaultFixture(async (fixture) => { + fixture.log('Making sure we are clean before starting.'); + await fixture.cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setting up: creating a VPC with known tags'); + await fixture.cdkDeploy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setup complete!'); + fixture.log('Verifying we can now import that VPC'); + await fixture.cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' } }); +})); +test_helpers_1.integTest('Two ways of shoing the version', cdk_1.withDefaultFixture(async (fixture) => { + const version1 = await fixture.cdk(['version'], { verbose: false }); + const version2 = await fixture.cdk(['--version'], { verbose: false }); + expect(version1).toEqual(version2); +})); +test_helpers_1.integTest('Termination protection', cdk_1.withDefaultFixture(async (fixture) => { + const stackName = 'termination-protection'; + await fixture.cdkDeploy(stackName); + // Try a destroy that should fail + await expect(fixture.cdkDestroy(stackName)).rejects.toThrow('exited with error'); + // Can update termination protection even though the change set doesn't contain changes + await fixture.cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); + await fixture.cdkDestroy(stackName); +})); +test_helpers_1.integTest('cdk synth', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', fixture.fullStackName('test-1')]); + const template1 = await readTemplate(fixture, 'test-1'); + expect(template1).toEqual({ + Resources: { + topic69831491: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-1/topic/Resource`, + }, + }, + }, + }); + await fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false }); + const template2 = await readTemplate(fixture, 'test-2'); + expect(template2).toEqual({ + Resources: { + topic152D84A37: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic1/Resource`, + }, + }, + topic2A4FB547F: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic2/Resource`, + }, + }, + }, + }); +})); +test_helpers_1.integTest('ssm parameter provider error', cdk_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdk(['synth', + fixture.fullStackName('missing-ssm-parameter'), + '-c', 'test:ssm-parameter-name=/does/not/exist'], { + allowErrExit: true, + })).resolves.toContain('SSM parameter not available in account'); +})); +test_helpers_1.integTest('automatic ordering', cdk_1.withDefaultFixture(async (fixture) => { + // Deploy the consuming stack which will include the producing stack + await fixture.cdkDeploy('order-consuming'); + // Destroy the providing stack which will include the consuming stack + await fixture.cdkDestroy('order-providing'); +})); +test_helpers_1.integTest('context setting', cdk_1.withDefaultFixture(async (fixture) => { + await fs_1.promises.writeFile(path.join(fixture.integTestDir, 'cdk.context.json'), JSON.stringify({ + contextkey: 'this is the context value', + })); + try { + await expect(fixture.cdk(['context'])).resolves.toContain('this is the context value'); + // Test that deleting the contextkey works + await fixture.cdk(['context', '--reset', 'contextkey']); + await expect(fixture.cdk(['context'])).resolves.not.toContain('this is the context value'); + // Test that forced delete of the context key does not throw + await fixture.cdk(['context', '-f', '--reset', 'contextkey']); + } + finally { + await fs_1.promises.unlink(path.join(fixture.integTestDir, 'cdk.context.json')); + } +})); +test_helpers_1.integTest('context in stage propagates to top', cdk_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdkSynth({ + // This will make it error to prove that the context bubbles up, and also that we can fail on command + options: ['--no-lookups'], + modEnv: { + INTEG_STACK_SET: 'stage-using-context', + }, + allowErrExit: true, + })).resolves.toContain('Context lookups have been disabled'); +})); +test_helpers_1.integTest('deploy', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('test-2', { captureStderr: false }); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(2); +})); +test_helpers_1.integTest('deploy all', cdk_1.withDefaultFixture(async (fixture) => { + const arns = await fixture.cdkDeploy('test-*', { captureStderr: false }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(arns.split('\n').length).toEqual(2); +})); +test_helpers_1.integTest('nested stack with parameters', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances + // of this test to run in parallel, othewise they will attempt to create the same SNS topic. + const stackArn = await fixture.cdkDeploy('with-nested-stack-using-parameters', { + options: ['--parameters', `MyTopicParam=${fixture.stackNamePrefix}ThereIsNoSpoon`], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(1); +})); +test_helpers_1.integTest('deploy without execute a named change set', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const changeSetName = 'custom-change-set-name'; + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--no-execute', '--change-set-name', changeSetName], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); + //verify a change set was created with the provided name + const changeSetResponse = await fixture.aws.cloudFormation('listChangeSets', { + StackName: stackArn, + }); + const changeSets = changeSetResponse.Summaries || []; + expect(changeSets.length).toEqual(1); + expect(changeSets[0].ChangeSetName).toEqual(changeSetName); + expect(changeSets[0].Status).toEqual('CREATE_COMPLETE'); +})); +test_helpers_1.integTest('security related changes without a CLI are expected to fail', cdk_1.withDefaultFixture(async (fixture) => { + // redirect /dev/null to stdin, which means there will not be tty attached + // since this stack includes security-related changes, the deployment should + // immediately fail because we can't confirm the changes + const stackName = 'iam-test'; + await expect(fixture.cdkDeploy(stackName, { + options: ['<', '/dev/null'], + neverRequireApproval: false, + })).rejects.toThrow('exited with error'); + // Ensure stack was not deployed + await expect(fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName(stackName), + })).rejects.toThrow('does not exist'); +})); +test_helpers_1.integTest('deploy wildcard with outputs', cdk_1.withDefaultFixture(async (fixture) => { + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs_1.promises.mkdir(path.dirname(outputsFile), { recursive: true }); + await fixture.cdkDeploy(['outputs-test-*'], { + options: ['--outputs-file', outputsFile], + }); + const outputs = JSON.parse((await fs_1.promises.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + expect(outputs).toEqual({ + [`${fixture.stackNamePrefix}-outputs-test-1`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-1MyTopic`, + }, + [`${fixture.stackNamePrefix}-outputs-test-2`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-2MyOtherTopic`, + }, + }); +})); +test_helpers_1.integTest('deploy with parameters', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}bazinga`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}bazinga`, + }, + ]); +})); +test_helpers_1.integTest('update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('param-test-1'), + }); + const stackArn = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackId; + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('ROLLBACK_COMPLETE'); + // WHEN + const newStackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + const newStackResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: newStackArn, + }); + // THEN + expect(stackArn).not.toEqual(newStackArn); // new stack was created + expect((_c = newStackResponse.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('CREATE_COMPLETE'); + expect((_d = newStackResponse.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('stack in UPDATE_ROLLBACK_COMPLETE state can be updated', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}nice`, + ], + captureStderr: false, + }); + let response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('CREATE_COMPLETE'); + // bad parameter name with @ will put stack into UPDATE_ROLLBACK_COMPLETE + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + ; + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('UPDATE_ROLLBACK_COMPLETE'); + // WHEN + await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + // THEN + expect((_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('UPDATE_COMPLETE'); + expect((_d = response.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('deploy with wildcard and parameters', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('param-test-*', { + options: [ + '--parameters', `${fixture.stackNamePrefix}-param-test-1:TopicNameParam=${fixture.stackNamePrefix}bazinga`, + '--parameters', `${fixture.stackNamePrefix}-param-test-2:OtherTopicNameParam=${fixture.stackNamePrefix}ThatsMySpot`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:DisplayNameParam=${fixture.stackNamePrefix}HeyThere`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:OtherDisplayNameParam=${fixture.stackNamePrefix}AnotherOne`, + ], + }); +})); +test_helpers_1.integTest('deploy with parameters multi', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const paramVal1 = `${fixture.stackNamePrefix}bazinga`; + const paramVal2 = `${fixture.stackNamePrefix}=jagshemash`; + const stackArn = await fixture.cdkDeploy('param-test-3', { + options: [ + '--parameters', `DisplayNameParam=${paramVal1}`, + '--parameters', `OtherDisplayNameParam=${paramVal2}`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'DisplayNameParam', + ParameterValue: paramVal1, + }, + { + ParameterKey: 'OtherDisplayNameParam', + ParameterValue: paramVal2, + }, + ]); +})); +test_helpers_1.integTest('deploy with notification ARN', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const topicName = `${fixture.stackNamePrefix}-test-topic`; + const response = await fixture.aws.sns('createTopic', { Name: topicName }); + const topicArn = response.TopicArn; + try { + await fixture.cdkDeploy('test-2', { + options: ['--notification-arns', topicArn], + }); + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('test-2'), + }); + expect((_a = describeResponse.Stacks) === null || _a === void 0 ? void 0 : _a[0].NotificationARNs).toEqual([topicArn]); + } + finally { + await fixture.aws.sns('deleteTopic', { + TopicArn: topicArn, + }); + } +})); +test_helpers_1.integTest('deploy with role', cdk_1.withDefaultFixture(async (fixture) => { + const roleName = `${fixture.stackNamePrefix}-test-role`; + await deleteRole(); + const createResponse = await fixture.aws.iam('createRole', { + RoleName: roleName, + AssumeRolePolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: 'sts:AssumeRole', + Principal: { Service: 'cloudformation.amazonaws.com' }, + Effect: 'Allow', + }, { + Action: 'sts:AssumeRole', + Principal: { AWS: (await fixture.aws.sts('getCallerIdentity', {})).Arn }, + Effect: 'Allow', + }], + }), + }); + const roleArn = createResponse.Role.Arn; + try { + await fixture.aws.iam('putRolePolicy', { + RoleName: roleName, + PolicyName: 'DefaultPolicy', + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: '*', + Resource: '*', + Effect: 'Allow', + }], + }), + }); + await aws_1.retry(fixture.output, 'Trying to assume fresh role', aws_1.retry.forSeconds(300), async () => { + await fixture.aws.sts('assumeRole', { + RoleArn: roleArn, + RoleSessionName: 'testing', + }); + }); + // In principle, the role has replicated from 'us-east-1' to wherever we're testing. + // Give it a little more sleep to make sure CloudFormation is not hitting a box + // that doesn't have it yet. + await aws_1.sleep(5000); + await fixture.cdkDeploy('test-2', { + options: ['--role-arn', roleArn], + }); + // Immediately delete the stack again before we delete the role. + // + // Since roles are sticky, if we delete the role before the stack, subsequent DeleteStack + // operations will fail when CloudFormation tries to assume the role that's already gone. + await fixture.cdkDestroy('test-2'); + } + finally { + await deleteRole(); + } + async function deleteRole() { + try { + for (const policyName of (await fixture.aws.iam('listRolePolicies', { RoleName: roleName })).PolicyNames) { + await fixture.aws.iam('deleteRolePolicy', { + RoleName: roleName, + PolicyName: policyName, + }); + } + await fixture.aws.iam('deleteRole', { RoleName: roleName }); + } + catch (e) { + if (e.message.indexOf('cannot be found') > -1) { + return; + } + throw e; + } + } +})); +test_helpers_1.integTest('cdk diff', cdk_1.withDefaultFixture(async (fixture) => { + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // We can make it fail by passing --fail + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1')])) + .rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail on multiple stacks exits with error if any of the stacks contains a diff', cdk_1.withDefaultFixture(async (fixture) => { + // GIVEN + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + await fixture.cdkDeploy('test-2'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('There were no differences'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail with multiple stack exits with if any of the stacks contains a diff', cdk_1.withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('test-1'); + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('There were no differences'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --security-only --fail exits when security changes are present', cdk_1.withDefaultFixture(async (fixture) => { + const stackName = 'iam-test'; + await expect(fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName(stackName)])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('deploy stack with docker asset', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('docker'); +})); +test_helpers_1.integTest('deploy and test stack with lambda asset', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b; + const stackArn = await fixture.cdkDeploy('lambda', { captureStderr: false }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + const lambdaArn = (_b = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Outputs) === null || _b === void 0 ? void 0 : _b[0].OutputValue; + if (lambdaArn === undefined) { + throw new Error('Stack did not have expected Lambda ARN output'); + } + const output = await fixture.aws.lambda('invoke', { + FunctionName: lambdaArn, + }); + expect(JSON.stringify(output.Payload)).toContain('dear asset'); +})); +test_helpers_1.integTest('cdk ls', cdk_1.withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls'], { captureStderr: false }); + const expectedStacks = [ + 'conditional-resource', + 'docker', + 'docker-with-custom-file', + 'failed', + 'iam-test', + 'lambda', + 'missing-ssm-parameter', + 'order-providing', + 'outputs-test-1', + 'outputs-test-2', + 'param-test-1', + 'param-test-2', + 'param-test-3', + 'termination-protection', + 'test-1', + 'test-2', + 'with-nested-stack', + 'with-nested-stack-using-parameters', + 'order-consuming', + ]; + for (const stack of expectedStacks) { + expect(listing).toContain(fixture.fullStackName(stack)); + } +})); +test_helpers_1.integTest('synthing a stage with errors leads to failure', cdk_1.withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['synth'], { + allowErrExit: true, + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', + }, + }); + expect(output).toContain('This is an error'); +})); +test_helpers_1.integTest('synthing a stage with errors can be suppressed', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', '--no-validation'], { + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', + }, + }); +})); +test_helpers_1.integTest('deploy stack without resource', cdk_1.withDefaultFixture(async (fixture) => { + // Deploy the stack without resources + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + // This should have succeeded but not deployed the stack. + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); + // Deploy the stack with resources + await fixture.cdkDeploy('conditional-resource'); + // Then again WITHOUT resources (this should destroy the stack) + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); +})); +test_helpers_1.integTest('IAM diff', cdk_1.withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]); + // Roughly check for a table like this: + // + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ + // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ + expect(output).toContain('${SomeRole.Arn}'); + expect(output).toContain('sts:AssumeRole'); + expect(output).toContain('ec2.amazonaws.com'); +})); +test_helpers_1.integTest('fast deploy', cdk_1.withDefaultFixture(async (fixture) => { + // we are using a stack with a nested stack because CFN will always attempt to + // update a nested stack, which will allow us to verify that updates are actually + // skipped unless --force is specified. + const stackArn = await fixture.cdkDeploy('with-nested-stack', { captureStderr: false }); + const changeSet1 = await getLatestChangeSet(); + // Deploy the same stack again, there should be no new change set created + await fixture.cdkDeploy('with-nested-stack'); + const changeSet2 = await getLatestChangeSet(); + expect(changeSet2.ChangeSetId).toEqual(changeSet1.ChangeSetId); + // Deploy the stack again with --force, now we should create a changeset + await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] }); + const changeSet3 = await getLatestChangeSet(); + expect(changeSet3.ChangeSetId).not.toEqual(changeSet2.ChangeSetId); + // Deploy the stack again with tags, expected to create a new changeset + // even though the resources didn't change. + await fixture.cdkDeploy('with-nested-stack', { options: ['--tags', 'key=value'] }); + const changeSet4 = await getLatestChangeSet(); + expect(changeSet4.ChangeSetId).not.toEqual(changeSet3.ChangeSetId); + async function getLatestChangeSet() { + var _a, _b, _c; + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn }); + if (!((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0])) { + throw new Error('Did not get a ChangeSet at all'); + } + fixture.log(`Found Change Set ${(_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].ChangeSetId}`); + return (_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0]; + } +})); +test_helpers_1.integTest('failed deploy does not hang', cdk_1.withDefaultFixture(async (fixture) => { + // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. + await expect(fixture.cdkDeploy('failed')).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('can still load old assemblies', cdk_1.withDefaultFixture(async (fixture) => { + const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx'); + const testAssembliesDirectory = path.join(__dirname, 'cloud-assemblies'); + for (const asmdir of await listChildDirs(testAssembliesDirectory)) { + fixture.log(`ASSEMBLY ${asmdir}`); + await cdk_1.cloneDirectory(asmdir, cxAsmDir); + // Some files in the asm directory that have a .js extension are + // actually treated as templates. Evaluate them using NodeJS. + const templates = await listChildren(cxAsmDir, fullPath => Promise.resolve(fullPath.endsWith('.js'))); + for (const template of templates) { + const targetName = template.replace(/.js$/, ''); + await cdk_1.shell([process.execPath, template, '>', targetName], { + cwd: cxAsmDir, + output: fixture.output, + modEnv: { + TEST_ACCOUNT: await fixture.aws.account(), + TEST_REGION: fixture.aws.region, + }, + }); + } + // Use this directory as a Cloud Assembly + const output = await fixture.cdk([ + '--app', cxAsmDir, + '-v', + 'synth', + ]); + // Assert that there was no providerError in CDK's stderr + // Because we rely on the app/framework to actually error in case the + // provider fails, we inspect the logs here. + expect(output).not.toContain('$providerError'); + } +})); +test_helpers_1.integTest('generating and loading assembly', cdk_1.withDefaultFixture(async (fixture) => { + const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`; + await fixture.shell(['rm', '-rf', asmOutputDir]); + // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory. + await fixture.cdk(['synth']); + await fixture.cdk(['synth', '--output', asmOutputDir]); + // cdk.out in the current directory and the indicated --output should be the same + await fixture.shell(['diff', 'cdk.out', asmOutputDir]); + // Check that we can 'ls' the synthesized asm. + // Change to some random directory to make sure we're not accidentally loading cdk.json + const list = await fixture.cdk(['--app', asmOutputDir, 'ls'], { cwd: os.tmpdir() }); + // Same stacks we know are in the app + expect(list).toContain(`${fixture.stackNamePrefix}-lambda`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-1`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-2`); + // Check that we can use '.' and just synth ,the generated asm + const stackTemplate = await fixture.cdk(['--app', '.', 'synth', fixture.fullStackName('test-2')], { + cwd: asmOutputDir, + }); + expect(stackTemplate).toContain('topic152D84A37'); + // Deploy a Lambda from the copied asm + await fixture.cdkDeploy('lambda', { options: ['-a', '.'], cwd: asmOutputDir }); + // Remove (rename) the original custom docker file that was used during synth. + // this verifies that the assemly has a copy of it and that the manifest uses + // relative paths to reference to it. + const customDockerFile = path.join(fixture.integTestDir, 'docker', 'Dockerfile.Custom'); + await fs_1.promises.rename(customDockerFile, `${customDockerFile}~`); + try { + // deploy a docker image with custom file without synth (uses assets) + await fixture.cdkDeploy('docker-with-custom-file', { options: ['-a', '.'], cwd: asmOutputDir }); + } + finally { + // Rename back to restore fixture to original state + await fs_1.promises.rename(`${customDockerFile}~`, customDockerFile); + } +})); +test_helpers_1.integTest('templates on disk contain metadata resource, also in nested assemblies', cdk_1.withDefaultFixture(async (fixture) => { + // Synth first, and switch on version reporting because cdk.json is disabling it + await fixture.cdk(['synth', '--version-reporting=true']); + // Load template from disk from root assembly + const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']); + expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); + // Load template from nested assembly + const nestedTemplateContents = await fixture.shell(['cat', 'cdk.out/assembly-*-stage/*-stage-StackInStage.template.json']); + expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); +})); +async function listChildren(parent, pred) { + const ret = new Array(); + for (const child of await fs_1.promises.readdir(parent, { encoding: 'utf-8' })) { + const fullPath = path.join(parent, child.toString()); + if (await pred(fullPath)) { + ret.push(fullPath); + } + } + return ret; +} +async function listChildDirs(parent) { + return listChildren(parent, async (fullPath) => (await fs_1.promises.stat(fullPath)).isDirectory()); +} +async function readTemplate(fixture, stackName) { + const fullStackName = fixture.fullStackName(stackName); + const templatePath = path.join(fixture.integTestDir, 'cdk.out', `${fullStackName}.template.json`); + return JSON.parse((await fs_1.promises.readFile(templatePath, { encoding: 'utf-8' })).toString()); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLmludGVndGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNsaS5pbnRlZ3Rlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSwyQkFBb0M7QUFDcEMseUJBQXlCO0FBQ3pCLDZCQUE2QjtBQUM3Qix3Q0FBOEM7QUFDOUMsd0NBQXdGO0FBQ3hGLDBEQUFvRDtBQUVwRCxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsR0FBRyxJQUFJLENBQUMsQ0FBQztBQUU1Qix3QkFBUyxDQUFDLFlBQVksRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDM0QsT0FBTyxDQUFDLEdBQUcsQ0FBQywyQ0FBMkMsQ0FBQyxDQUFDO0lBQ3pELE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFFckYsT0FBTyxDQUFDLEdBQUcsQ0FBQyw0Q0FBNEMsQ0FBQyxDQUFDO0lBQzFELE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDcEYsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRS9CLE9BQU8sQ0FBQyxHQUFHLENBQUMsc0NBQXNDLENBQUMsQ0FBQztJQUNwRCxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsWUFBWSxFQUFFLEVBQUUsTUFBTSxFQUFFLEVBQUUsa0JBQWtCLEVBQUUsUUFBUSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBQ3RGLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGdDQUFnQyxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUMvRSxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFH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YUFBYSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQ3ZELE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksRUFBRSxTQUFTLEVBQUUsR0FBRyxhQUFhLGdCQUFnQixDQUFDLENBQUM7SUFDbEcsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsTUFBTSxhQUFFLENBQUMsUUFBUSxDQUFDLFlBQVksRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztBQUN6RixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgcHJvbWlzZXMgYXMgZnMgfSBmcm9tICdmcyc7XG5pbXBvcnQgKiBhcyBvcyBmcm9tICdvcyc7XG5pbXBvcnQgKiBhcyBwYXRoIGZyb20gJ3BhdGgnO1xuaW1wb3J0IHsgcmV0cnksIHNsZWVwIH0gZnJvbSAnLi4vaGVscGVycy9hd3MnO1xuaW1wb3J0IHsgY2xvbmVEaXJlY3RvcnksIHNoZWxsLCB3aXRoRGVmYXVsdEZpeHR1cmUsIFRlc3RGaXh0dXJlIH0gZnJvbSAnLi4vaGVscGVycy9jZGsnO1xuaW1wb3J0IHsgaW50ZWdUZXN0IH0gZnJvbSAnLi4vaGVscGVycy90ZXN0LWhlbHBlcnMnO1xuXG5qZXN0LnNldFRpbWVvdXQoNjAwICogMTAwMCk7XG5cbmludGVnVGVzdCgnVlBDIExvb2t1cCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBmaXh0dXJlLmxvZygnTWFraW5nIHN1cmUgd2UgYXJlIGNsZWFuIGJlZm9yZSBzdGFydGluZy4nKTtcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXN0cm95KCdkZWZpbmUtdnBjJywgeyBtb2RFbnY6IHsgRU5BQkxFX1ZQQ19URVNUSU5HOiAnREVGSU5FJyB9IH0pO1xuXG4gIGZpeHR1cmUubG9nKCdTZXR0aW5nIHVwOiBjcmVhdGluZyBhIFZQQyB3aXRoIGtub3duIHRhZ3MnKTtcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ2RlZmluZS12cGMnLCB7IG1vZEVudjogeyBFTkFCTEVfVlBDX1RFU1RJTkc6ICdERUZJTkUnIH0gfSk7XG4gIGZpeHR1cmUubG9nKCdTZXR1cCBjb21wbGV0ZSEnKTtcblxuICBmaXh0dXJlLmxvZygnVmVyaWZ5aW5nIHdlIGNhbiBub3cgaW1wb3J0IHRoYXQgVlBDJyk7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdpbXBvcnQtdnBjJywgeyBtb2RFbnY6IHsgRU5BQkxFX1ZQQ19URVNUSU5HOiAnSU1QT1JUJyB9IH0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ1R3byB3YXlzIG9mIHNob2luZyB0aGUgdmVyc2lvbicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCB2ZXJzaW9uMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsndmVyc2lvbiddLCB7IHZlcmJvc2U6IGZhbHNlIH0pO1xuICBjb25zdCB2ZXJzaW9uMiA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnLS12ZXJzaW9uJ10sIHsgdmVyYm9zZTogZmFsc2UgfSk7XG5cbiAgZXhwZWN0KHZlcnNpb24xKS50b0VxdWFsKHZlcnNpb24yKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdUZXJtaW5hdGlvbiBwcm90ZWN0aW9uJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrTmFtZSA9ICd0ZXJtaW5hdGlvbi1wcm90ZWN0aW9uJztcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lKTtcblxuICAvLyBUcnkgYSBkZXN0cm95IHRoYXQgc2hvdWxkIGZhaWxcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrRGVzdHJveShzdGFja05hbWUpKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG5cbiAgLy8gQ2FuIHVwZGF0ZSB0ZXJtaW5hdGlvbiBwcm90ZWN0aW9uIGV2ZW4gdGhvdWdoIHRoZSBjaGFuZ2Ugc2V0IGRvZXNuJ3QgY29udGFpbiBjaGFuZ2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KHN0YWNrTmFtZSwgeyBtb2RFbnY6IHsgVEVSTUlOQVRJT05fUFJPVEVDVElPTjogJ0ZBTFNFJyB9IH0pO1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koc3RhY2tOYW1lKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgc3ludGgnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgY29uc3QgdGVtcGxhdGUxID0gYXdhaXQgcmVhZFRlbXBsYXRlKGZpeHR1cmUsICd0ZXN0LTEnKTtcbiAgZXhwZWN0KHRlbXBsYXRlMSkudG9FcXVhbCh7XG4gICAgUmVzb3VyY2VzOiB7XG4gICAgICB0b3BpYzY5ODMxNDkxOiB7XG4gICAgICAgIFR5cGU6ICdBV1M6OlNOUzo6VG9waWMnLFxuICAgICAgICBNZXRhZGF0YToge1xuICAgICAgICAgICdhd3M6Y2RrOnBhdGgnOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0xL3RvcGljL1Jlc291cmNlYCxcbiAgICAgICAgfSxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSk7XG5cbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyldLCB7IHZlcmJvc2U6IGZhbHNlIH0pO1xuICBjb25zdCB0ZW1wbGF0ZTIgPSBhd2FpdCByZWFkVGVtcGxhdGUoZml4dHVyZSwgJ3Rlc3QtMicpO1xuICBleHBlY3QodGVtcGxhdGUyKS50b0VxdWFsKHtcbiAgICBSZXNvdXJjZXM6IHtcbiAgICAgIHRvcGljMTUyRDg0QTM3OiB7XG4gICAgICAgIFR5cGU6ICdBV1M6OlNOUzo6VG9waWMnLFxuICAgICAgICBNZXRhZGF0YToge1xuICAgICAgICAgICdhd3M6Y2RrOnBhdGgnOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0yL3RvcGljMS9SZXNvdXJjZWAsXG4gICAgICAgIH0sXG4gICAgICB9LFxuICAgICAgdG9waWMyQTRGQjU0N0Y6IHtcbiAgICAgICAgVHlwZTogJ0FXUzo6U05TOjpUb3BpYycsXG4gICAgICAgIE1ldGFkYXRhOiB7XG4gICAgICAgICAgJ2F3czpjZGs6cGF0aCc6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS10ZXN0LTIvdG9waWMyL1Jlc291cmNlYCxcbiAgICAgICAgfSxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSk7XG5cbn0pKTtcblxuaW50ZWdUZXN0KCdzc20gcGFyYW1ldGVyIHByb3ZpZGVyIGVycm9yJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ3N5bnRoJyxcbiAgICBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ21pc3Npbmctc3NtLXBhcmFtZXRlcicpLFxuICAgICctYycsICd0ZXN0OnNzbS1wYXJhbWV0ZXItbmFtZT0vZG9lcy9ub3QvZXhpc3QnXSwge1xuICAgIGFsbG93RXJyRXhpdDogdHJ1ZSxcbiAgfSkpLnJlc29sdmVzLnRvQ29udGFpbignU1NNIHBhcmFtZXRlciBub3QgYXZhaWxhYmxlIGluIGFjY291bnQnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdhdXRvbWF0aWMgb3JkZXJpbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gRGVwbG95IHRoZSBjb25zdW1pbmcgc3RhY2sgd2hpY2ggd2lsbCBpbmNsdWRlIHRoZSBwcm9kdWNpbmcgc3RhY2tcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ29yZGVyLWNvbnN1bWluZycpO1xuXG4gIC8vIERlc3Ryb3kgdGhlIHByb3ZpZGluZyBzdGFjayB3aGljaCB3aWxsIGluY2x1ZGUgdGhlIGNvbnN1bWluZyBzdGFja1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koJ29yZGVyLXByb3ZpZGluZycpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NvbnRleHQgc2V0dGluZycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBmcy53cml0ZUZpbGUocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpLCBKU09OLnN0cmluZ2lmeSh7XG4gICAgY29udGV4dGtleTogJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnLFxuICB9KSk7XG4gIHRyeSB7XG4gICAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrKFsnY29udGV4dCddKSkucmVzb2x2ZXMudG9Db250YWluKCd0aGlzIGlzIHRoZSBjb250ZXh0IHZhbHVlJyk7XG5cbiAgICAvLyBUZXN0IHRoYXQgZGVsZXRpbmcgdGhlIGNvbnRleHRrZXkgd29ya3NcbiAgICBhd2FpdCBmaXh0dXJlLmNkayhbJ2NvbnRleHQnLCAnLS1yZXNldCcsICdjb250ZXh0a2V5J10pO1xuICAgIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2NvbnRleHQnXSkpLnJlc29sdmVzLm5vdC50b0NvbnRhaW4oJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnKTtcblxuICAgIC8vIFRlc3QgdGhhdCBmb3JjZWQgZGVsZXRlIG9mIHRoZSBjb250ZXh0IGtleSBkb2VzIG5vdCB0aHJvd1xuICAgIGF3YWl0IGZpeHR1cmUuY2RrKFsnY29udGV4dCcsICctZicsICctLXJlc2V0JywgJ2NvbnRleHRrZXknXSk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmcy51bmxpbmsocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NvbnRleHQgaW4gc3RhZ2UgcHJvcGFnYXRlcyB0byB0b3AnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrU3ludGgoe1xuICAgIC8vIFRoaXMgd2lsbCBtYWtlIGl0IGVycm9yIHRvIHByb3ZlIHRoYXQgdGhlIGNvbnRleHQgYnViYmxlcyB1cCwgYW5kIGFsc28gdGhhdCB3ZSBjYW4gZmFpbCBvbiBjb21tYW5kXG4gICAgb3B0aW9uczogWyctLW5vLWxvb2t1cHMnXSxcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXVzaW5nLWNvbnRleHQnLFxuICAgIH0sXG4gICAgYWxsb3dFcnJFeGl0OiB0cnVlLFxuICB9KSkucmVzb2x2ZXMudG9Db250YWluKCdDb250ZXh0IGxvb2t1cHMgaGF2ZSBiZWVuIGRpc2FibGVkJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMicsIHsgY2FwdHVyZVN0ZGVycjogZmFsc2UgfSk7XG5cbiAgLy8gdmVyaWZ5IHRoZSBudW1iZXIgb2YgcmVzb3VyY2VzIGluIHRoZSBzdGFja1xuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrUmVzb3VyY2VzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tSZXNvdXJjZXM/Lmxlbmd0aCkudG9FcXVhbCgyKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgYWxsJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGFybnMgPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0qJywgeyBjYXB0dXJlU3RkZXJyOiBmYWxzZSB9KTtcblxuICAvLyB2ZXJpZnkgdGhhdCB3ZSBvbmx5IGRlcGxveWVkIGEgc2luZ2xlIHN0YWNrICh0aGVyZSdzIGEgc2luZ2xlIEFSTiBpbiB0aGUgb3V0cHV0KVxuICBleHBlY3QoYXJucy5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDIpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ25lc3RlZCBzdGFjayB3aXRoIHBhcmFtZXRlcnMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gU1RBQ0tfTkFNRV9QUkVGSVggaXMgdXNlZCBpbiBNeVRvcGljUGFyYW0gdG8gYWxsb3cgbXVsdGlwbGUgaW5zdGFuY2VzXG4gIC8vIG9mIHRoaXMgdGVzdCB0byBydW4gaW4gcGFyYWxsZWwsIG90aGV3aXNlIHRoZXkgd2lsbCBhdHRlbXB0IHRvIGNyZWF0ZSB0aGUgc2FtZSBTTlMgdG9waWMuXG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3dpdGgtbmVzdGVkLXN0YWNrLXVzaW5nLXBhcmFtZXRlcnMnLCB7XG4gICAgb3B0aW9uczogWyctLXBhcmFtZXRlcnMnLCBgTXlUb3BpY1BhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9VGhlcmVJc05vU3Bvb25gXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgLy8gdmVyaWZ5IHRoYXQgd2Ugb25seSBkZXBsb3llZCBhIHNpbmdsZSBzdGFjayAodGhlcmUncyBhIHNpbmdsZSBBUk4gaW4gdGhlIG91dHB1dClcbiAgZXhwZWN0KHN0YWNrQXJuLnNwbGl0KCdcXG4nKS5sZW5ndGgpLnRvRXF1YWwoMSk7XG5cbiAgLy8gdmVyaWZ5IHRoZSBudW1iZXIgb2YgcmVzb3VyY2VzIGluIHRoZSBzdGFja1xuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrUmVzb3VyY2VzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tSZXNvdXJjZXM/Lmxlbmd0aCkudG9FcXVhbCgxKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgd2l0aG91dCBleGVjdXRlIGEgbmFtZWQgY2hhbmdlIHNldCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBjaGFuZ2VTZXROYW1lID0gJ2N1c3RvbS1jaGFuZ2Utc2V0LW5hbWUnO1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCd0ZXN0LTInLCB7XG4gICAgb3B0aW9uczogWyctLW5vLWV4ZWN1dGUnLCAnLS1jaGFuZ2Utc2V0LW5hbWUnLCBjaGFuZ2VTZXROYW1lXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG4gIC8vIHZlcmlmeSB0aGF0IHdlIG9ubHkgZGVwbG95ZWQgYSBzaW5nbGUgc3RhY2sgKHRoZXJlJ3MgYSBzaW5nbGUgQVJOIGluIHRoZSBvdXRwdXQpXG4gIGV4cGVjdChzdGFja0Fybi5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDEpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1JFVklFV19JTl9QUk9HUkVTUycpO1xuXG4gIC8vdmVyaWZ5IGEgY2hhbmdlIHNldCB3YXMgY3JlYXRlZCB3aXRoIHRoZSBwcm92aWRlZCBuYW1lXG4gIGNvbnN0IGNoYW5nZVNldFJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2xpc3RDaGFuZ2VTZXRzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBjb25zdCBjaGFuZ2VTZXRzID0gY2hhbmdlU2V0UmVzcG9uc2UuU3VtbWFyaWVzIHx8IFtdO1xuICBleHBlY3QoY2hhbmdlU2V0cy5sZW5ndGgpLnRvRXF1YWwoMSk7XG4gIGV4cGVjdChjaGFuZ2VTZXRzWzBdLkNoYW5nZVNldE5hbWUpLnRvRXF1YWwoY2hhbmdlU2V0TmFtZSk7XG4gIGV4cGVjdChjaGFuZ2VTZXRzWzBdLlN0YXR1cykudG9FcXVhbCgnQ1JFQVRFX0NPTVBMRVRFJyk7XG59KSk7XG5cbmludGVnVGVzdCgnc2VjdXJpdHkgcmVsYXRlZCBjaGFuZ2VzIHdpdGhvdXQgYSBDTEkgYXJlIGV4cGVjdGVkIHRvIGZhaWwnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gcmVkaXJlY3QgL2Rldi9udWxsIHRvIHN0ZGluLCB3aGljaCBtZWFucyB0aGVyZSB3aWxsIG5vdCBiZSB0dHkgYXR0YWNoZWRcbiAgLy8gc2luY2UgdGhpcyBzdGFjayBpbmNsdWRlcyBzZWN1cml0eS1yZWxhdGVkIGNoYW5nZXMsIHRoZSBkZXBsb3ltZW50IHNob3VsZFxuICAvLyBpbW1lZGlhdGVseSBmYWlsIGJlY2F1c2Ugd2UgY2FuJ3QgY29uZmlybSB0aGUgY2hhbmdlc1xuICBjb25zdCBzdGFja05hbWUgPSAnaWFtLXRlc3QnO1xuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lLCB7XG4gICAgb3B0aW9uczogWyc8JywgJy9kZXYvbnVsbCddLCAvLyBINHgsIHRoaXMgb25seSB3b3JrcyBiZWNhdXNlIEkgaGFwcGVuIHRvIGtub3cgd2UgcGFzcyBzaGVsbDogdHJ1ZS5cbiAgICBuZXZlclJlcXVpcmVBcHByb3ZhbDogZmFsc2UsXG4gIH0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG5cbiAgLy8gRW5zdXJlIHN0YWNrIHdhcyBub3QgZGVwbG95ZWRcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFja05hbWUpLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdkb2VzIG5vdCBleGlzdCcpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aWxkY2FyZCB3aXRoIG91dHB1dHMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgb3V0cHV0c0ZpbGUgPSBwYXRoLmpvaW4oZml4dHVyZS5pbnRlZ1Rlc3REaXIsICdvdXRwdXRzJywgJ291dHB1dHMuanNvbicpO1xuICBhd2FpdCBmcy5ta2RpcihwYXRoLmRpcm5hbWUob3V0cHV0c0ZpbGUpLCB7IHJlY3Vyc2l2ZTogdHJ1ZSB9KTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveShbJ291dHB1dHMtdGVzdC0qJ10sIHtcbiAgICBvcHRpb25zOiBbJy0tb3V0cHV0cy1maWxlJywgb3V0cHV0c0ZpbGVdLFxuICB9KTtcblxuICBjb25zdCBvdXRwdXRzID0gSlNPTi5wYXJzZSgoYXdhaXQgZnMucmVhZEZpbGUob3V0cHV0c0ZpbGUsIHsgZW5jb2Rpbmc6ICd1dGYtOCcgfSkpLnRvU3RyaW5nKCkpO1xuICBleHBlY3Qob3V0cHV0cykudG9FcXVhbCh7XG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMWBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMU15VG9waWNgLFxuICAgIH0sXG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMmBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMk15T3RoZXJUb3BpY2AsXG4gICAgfSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWJhemluZ2FgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuXG4gIGV4cGVjdChyZXNwb25zZS5TdGFja3M/LlswXS5QYXJhbWV0ZXJzKS50b0VxdWFsKFtcbiAgICB7XG4gICAgICBQYXJhbWV0ZXJLZXk6ICdUb3BpY05hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YmF6aW5nYWAsXG4gICAgfSxcbiAgXSk7XG59KSk7XG5cbmludGVnVGVzdCgndXBkYXRlIHRvIHN0YWNrIGluIFJPTExCQUNLX0NPTVBMRVRFIHN0YXRlIHdpbGwgZGVsZXRlIHN0YWNrIGFuZCBjcmVhdGUgYSBuZXcgb25lJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIC8vIEdJVkVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogZml4dHVyZS5mdWxsU3RhY2tOYW1lKCdwYXJhbS10ZXN0LTEnKSxcbiAgfSk7XG5cbiAgY29uc3Qgc3RhY2tBcm4gPSByZXNwb25zZS5TdGFja3M/LlswXS5TdGFja0lkO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1JPTExCQUNLX0NPTVBMRVRFJyk7XG5cbiAgLy8gV0hFTlxuICBjb25zdCBuZXdTdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IG5ld1N0YWNrUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBuZXdTdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QgKHN0YWNrQXJuKS5ub3QudG9FcXVhbChuZXdTdGFja0Fybik7IC8vIG5ldyBzdGFjayB3YXMgY3JlYXRlZFxuICBleHBlY3QobmV3U3RhY2tSZXNwb25zZS5TdGFja3M/LlswXS5TdGFja1N0YXR1cykudG9FcXVhbCgnQ1JFQVRFX0NPTVBMRVRFJyk7XG4gIGV4cGVjdChuZXdTdGFja1Jlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ1RvcGljTmFtZVBhcmFtJyxcbiAgICAgIFBhcmFtZXRlclZhbHVlOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1hbGxnb29kYCxcbiAgICB9LFxuICBdKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzdGFjayBpbiBVUERBVEVfUk9MTEJBQ0tfQ09NUExFVEUgc3RhdGUgY2FuIGJlIHVwZGF0ZWQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1uaWNlYCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KTtcblxuICBsZXQgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlN0YWNrU3RhdHVzKS50b0VxdWFsKCdDUkVBVEVfQ09NUExFVEUnKTtcblxuICAvLyBiYWQgcGFyYW1ldGVyIG5hbWUgd2l0aCBAIHdpbGwgcHV0IHN0YWNrIGludG8gVVBEQVRFX1JPTExCQUNLX0NPTVBMRVRFXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpOztcblxuICByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcblxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9ST0xMQkFDS19DT01QTEVURScpO1xuXG4gIC8vIFdIRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYFRvcGljTmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YWxsZ29vZGAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9DT01QTEVURScpO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uUGFyYW1ldGVycykudG9FcXVhbChbXG4gICAge1xuICAgICAgUGFyYW1ldGVyS2V5OiAnVG9waWNOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIHdpbGRjYXJkIGFuZCBwYXJhbWV0ZXJzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LSonLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTE6VG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tcGFyYW0tdGVzdC0yOk90aGVyVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1UaGF0c015U3BvdGAsXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXBhcmFtLXRlc3QtMzpEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9SGV5VGhlcmVgLFxuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTM6T3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9QW5vdGhlck9uZWAsXG4gICAgXSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycyBtdWx0aScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBwYXJhbVZhbDEgPSBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYDtcbiAgY29uc3QgcGFyYW1WYWwyID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9PWphZ3NoZW1hc2hgO1xuXG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMycsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYERpc3BsYXlOYW1lUGFyYW09JHtwYXJhbVZhbDF9YCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgT3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7cGFyYW1WYWwyfWAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ0Rpc3BsYXlOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IHBhcmFtVmFsMSxcbiAgICB9LFxuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ090aGVyRGlzcGxheU5hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogcGFyYW1WYWwyLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIG5vdGlmaWNhdGlvbiBBUk4nLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgdG9waWNOYW1lID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtdG9waWNgO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3Muc25zKCdjcmVhdGVUb3BpYycsIHsgTmFtZTogdG9waWNOYW1lIH0pO1xuICBjb25zdCB0b3BpY0FybiA9IHJlc3BvbnNlLlRvcGljQXJuITtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJywge1xuICAgICAgb3B0aW9uczogWyctLW5vdGlmaWNhdGlvbi1hcm5zJywgdG9waWNBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gdmVyaWZ5IHRoYXQgdGhlIHN0YWNrIHdlIGRlcGxveWVkIGhhcyBvdXIgbm90aWZpY2F0aW9uIEFSTlxuICAgIGNvbnN0IGRlc2NyaWJlUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyksXG4gICAgfSk7XG4gICAgZXhwZWN0KGRlc2NyaWJlUmVzcG9uc2UuU3RhY2tzPy5bMF0uTm90aWZpY2F0aW9uQVJOcykudG9FcXVhbChbdG9waWNBcm5dKTtcbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5zbnMoJ2RlbGV0ZVRvcGljJywge1xuICAgICAgVG9waWNBcm46IHRvcGljQXJuLFxuICAgIH0pO1xuICB9XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcm9sZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCByb2xlTmFtZSA9IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS10ZXN0LXJvbGVgO1xuXG4gIGF3YWl0IGRlbGV0ZVJvbGUoKTtcblxuICBjb25zdCBjcmVhdGVSZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnY3JlYXRlUm9sZScsIHtcbiAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgQXNzdW1lUm9sZVBvbGljeURvY3VtZW50OiBKU09OLnN0cmluZ2lmeSh7XG4gICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICBTdGF0ZW1lbnQ6IFt7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IFNlcnZpY2U6ICdjbG91ZGZvcm1hdGlvbi5hbWF6b25hd3MuY29tJyB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9LCB7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IEFXUzogKGF3YWl0IGZpeHR1cmUuYXdzLnN0cygnZ2V0Q2FsbGVySWRlbnRpdHknLCB7fSkpLkFybiB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9XSxcbiAgICB9KSxcbiAgfSk7XG4gIGNvbnN0IHJvbGVBcm4gPSBjcmVhdGVSZXNwb25zZS5Sb2xlLkFybjtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ3B1dFJvbGVQb2xpY3knLCB7XG4gICAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgICBQb2xpY3lOYW1lOiAnRGVmYXVsdFBvbGljeScsXG4gICAgICBQb2xpY3lEb2N1bWVudDogSlNPTi5zdHJpbmdpZnkoe1xuICAgICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICAgIFN0YXRlbWVudDogW3tcbiAgICAgICAgICBBY3Rpb246ICcqJyxcbiAgICAgICAgICBSZXNvdXJjZTogJyonLFxuICAgICAgICAgIEVmZmVjdDogJ0FsbG93JyxcbiAgICAgICAgfV0sXG4gICAgICB9KSxcbiAgICB9KTtcblxuICAgIGF3YWl0IHJldHJ5KGZpeHR1cmUub3V0cHV0LCAnVHJ5aW5nIHRvIGFzc3VtZSBmcmVzaCByb2xlJywgcmV0cnkuZm9yU2Vjb25kcygzMDApLCBhc3luYyAoKSA9PiB7XG4gICAgICBhd2FpdCBmaXh0dXJlLmF3cy5zdHMoJ2Fzc3VtZVJvbGUnLCB7XG4gICAgICAgIFJvbGVBcm46IHJvbGVBcm4sXG4gICAgICAgIFJvbGVTZXNzaW9uTmFtZTogJ3Rlc3RpbmcnLFxuICAgICAgfSk7XG4gICAgfSk7XG5cbiAgICAvLyBJbiBwcmluY2lwbGUsIHRoZSByb2xlIGhhcyByZXBsaWNhdGVkIGZyb20gJ3VzLWVhc3QtMScgdG8gd2hlcmV2ZXIgd2UncmUgdGVzdGluZy5cbiAgICAvLyBHaXZlIGl0IGEgbGl0dGxlIG1vcmUgc2xlZXAgdG8gbWFrZSBzdXJlIENsb3VkRm9ybWF0aW9uIGlzIG5vdCBoaXR0aW5nIGEgYm94XG4gICAgLy8gdGhhdCBkb2Vzbid0IGhhdmUgaXQgeWV0LlxuICAgIGF3YWl0IHNsZWVwKDUwMDApO1xuXG4gICAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMicsIHtcbiAgICAgIG9wdGlvbnM6IFsnLS1yb2xlLWFybicsIHJvbGVBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gSW1tZWRpYXRlbHkgZGVsZXRlIHRoZSBzdGFjayBhZ2FpbiBiZWZvcmUgd2UgZGVsZXRlIHRoZSByb2xlLlxuICAgIC8vXG4gICAgLy8gU2luY2Ugcm9sZXMgYXJlIHN0aWNreSwgaWYgd2UgZGVsZXRlIHRoZSByb2xlIGJlZm9yZSB0aGUgc3RhY2ssIHN1YnNlcXVlbnQgRGVsZXRlU3RhY2tcbiAgICAvLyBvcGVyYXRpb25zIHdpbGwgZmFpbCB3aGVuIENsb3VkRm9ybWF0aW9uIHRyaWVzIHRvIGFzc3VtZSB0aGUgcm9sZSB0aGF0J3MgYWxyZWFkeSBnb25lLlxuICAgIGF3YWl0IGZpeHR1cmUuY2RrRGVzdHJveSgndGVzdC0yJyk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBkZWxldGVSb2xlKCk7XG4gIH1cblxuICBhc3luYyBmdW5jdGlvbiBkZWxldGVSb2xlKCkge1xuICAgIHRyeSB7XG4gICAgICBmb3IgKGNvbnN0IHBvbGljeU5hbWUgb2YgKGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnbGlzdFJvbGVQb2xpY2llcycsIHsgUm9sZU5hbWU6IHJvbGVOYW1lIH0pKS5Qb2xpY3lOYW1lcykge1xuICAgICAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ2RlbGV0ZVJvbGVQb2xpY3knLCB7XG4gICAgICAgICAgUm9sZU5hbWU6IHJvbGVOYW1lLFxuICAgICAgICAgIFBvbGljeU5hbWU6IHBvbGljeU5hbWUsXG4gICAgICAgIH0pO1xuICAgICAgfVxuICAgICAgYXdhaXQgZml4dHVyZS5hd3MuaWFtKCdkZWxldGVSb2xlJywgeyBSb2xlTmFtZTogcm9sZU5hbWUgfSk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgaWYgKGUubWVzc2FnZS5pbmRleE9mKCdjYW5ub3QgYmUgZm91bmQnKSA+IC0xKSB7IHJldHVybjsgfVxuICAgICAgdGhyb3cgZTtcbiAgICB9XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignQVdTOjpTTlM6OlRvcGljJyk7XG5cbiAgLy8gV2UgY2FuIG1ha2UgaXQgZmFpbCBieSBwYXNzaW5nIC0tZmFpbFxuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGsoWydkaWZmJywgJy0tZmFpbCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKSlcbiAgICAucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NkayBkaWZmIC0tZmFpbCBvbiBtdWx0aXBsZSBzdGFja3MgZXhpdHMgd2l0aCBlcnJvciBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3QgZGlmZjEgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMScpXSk7XG4gIGV4cGVjdChkaWZmMSkudG9Db250YWluKCdBV1M6OlNOUzo6VG9waWMnKTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJyk7XG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignVGhlcmUgd2VyZSBubyBkaWZmZXJlbmNlcycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnY2RrIGRpZmYgLS1mYWlsIHdpdGggbXVsdGlwbGUgc3RhY2sgZXhpdHMgd2l0aCBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMScpO1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ1RoZXJlIHdlcmUgbm8gZGlmZmVyZW5jZXMnKTtcblxuICBjb25zdCBkaWZmMiA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyldKTtcbiAgZXhwZWN0KGRpZmYyKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnY2RrIGRpZmYgLS1zZWN1cml0eS1vbmx5IC0tZmFpbCBleGl0cyB3aGVuIHNlY3VyaXR5IGNoYW5nZXMgYXJlIHByZXNlbnQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgc3RhY2tOYW1lID0gJ2lhbS10ZXN0JztcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrKFsnZGlmZicsICctLXNlY3VyaXR5LW9ubHknLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKHN0YWNrTmFtZSldKSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSBzdGFjayB3aXRoIGRvY2tlciBhc3NldCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZG9ja2VyJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IGFuZCB0ZXN0IHN0YWNrIHdpdGggbGFtYmRhIGFzc2V0Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ2xhbWJkYScsIHsgY2FwdHVyZVN0ZGVycjogZmFsc2UgfSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG4gIGNvbnN0IGxhbWJkYUFybiA9IHJlc3BvbnNlLlN0YWNrcz8uWzBdLk91dHB1dHM/LlswXS5PdXRwdXRWYWx1ZTtcbiAgaWYgKGxhbWJkYUFybiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdTdGFjayBkaWQgbm90IGhhdmUgZXhwZWN0ZWQgTGFtYmRhIEFSTiBvdXRwdXQnKTtcbiAgfVxuXG4gIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuYXdzLmxhbWJkYSgnaW52b2tlJywge1xuICAgIEZ1bmN0aW9uTmFtZTogbGFtYmRhQXJuLFxuICB9KTtcblxuICBleHBlY3QoSlNPTi5zdHJpbmdpZnkob3V0cHV0LlBheWxvYWQpKS50b0NvbnRhaW4oJ2RlYXIgYXNzZXQnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgbHMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgbGlzdGluZyA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnbHMnXSwgeyBjYXB0dXJlU3RkZXJyOiBmYWxzZSB9KTtcblxuICBjb25zdCBleHBlY3RlZFN0YWNrcyA9IFtcbiAgICAnY29uZGl0aW9uYWwtcmVzb3VyY2UnLFxuICAgICdkb2NrZXInLFxuICAgICdkb2NrZXItd2l0aC1jdXN0b20tZmlsZScsXG4gICAgJ2ZhaWxlZCcsXG4gICAgJ2lhbS10ZXN0JyxcbiAgICAnbGFtYmRhJyxcbiAgICAnbWlzc2luZy1zc20tcGFyYW1ldGVyJyxcbiAgICAnb3JkZXItcHJvdmlkaW5nJyxcbiAgICAnb3V0cHV0cy10ZXN0LTEnLFxuICAgICdvdXRwdXRzLXRlc3QtMicsXG4gICAgJ3BhcmFtLXRlc3QtMScsXG4gICAgJ3BhcmFtLXRlc3QtMicsXG4gICAgJ3BhcmFtLXRlc3QtMycsXG4gICAgJ3Rlcm1pbmF0aW9uLXByb3RlY3Rpb24nLFxuICAgICd0ZXN0LTEnLFxuICAgICd0ZXN0LTInLFxuICAgICd3aXRoLW5lc3RlZC1zdGFjaycsXG4gICAgJ3dpdGgtbmVzdGVkLXN0YWNrLXVzaW5nLXBhcmFtZXRlcnMnLFxuICAgICdvcmRlci1jb25zdW1pbmcnLFxuICBdO1xuXG4gIGZvciAoY29uc3Qgc3RhY2sgb2YgZXhwZWN0ZWRTdGFja3MpIHtcbiAgICBleHBlY3QobGlzdGluZykudG9Db250YWluKGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFjaykpO1xuICB9XG59KSk7XG5cbmludGVnVGVzdCgnc3ludGhpbmcgYSBzdGFnZSB3aXRoIGVycm9ycyBsZWFkcyB0byBmYWlsdXJlJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnc3ludGgnXSwge1xuICAgIGFsbG93RXJyRXhpdDogdHJ1ZSxcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXdpdGgtZXJyb3JzJyxcbiAgICB9LFxuICB9KTtcblxuICBleHBlY3Qob3V0cHV0KS50b0NvbnRhaW4oJ1RoaXMgaXMgYW4gZXJyb3InKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzeW50aGluZyBhIHN0YWdlIHdpdGggZXJyb3JzIGNhbiBiZSBzdXBwcmVzc2VkJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrKFsnc3ludGgnLCAnLS1uby12YWxpZGF0aW9uJ10sIHtcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXdpdGgtZXJyb3JzJyxcbiAgICB9LFxuICB9KTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgc3RhY2sgd2l0aG91dCByZXNvdXJjZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGhvdXQgcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScsIHsgbW9kRW52OiB7IE5PX1JFU09VUkNFOiAnVFJVRScgfSB9KTtcblxuICAvLyBUaGlzIHNob3VsZCBoYXZlIHN1Y2NlZWRlZCBidXQgbm90IGRlcGxveWVkIHRoZSBzdGFjay5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGggcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScpO1xuXG4gIC8vIFRoZW4gYWdhaW4gV0lUSE9VVCByZXNvdXJjZXMgKHRoaXMgc2hvdWxkIGRlc3Ryb3kgdGhlIHN0YWNrKVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnY29uZGl0aW9uYWwtcmVzb3VyY2UnLCB7IG1vZEVudjogeyBOT19SRVNPVVJDRTogJ1RSVUUnIH0gfSk7XG5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdJQU0gZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBvdXRwdXQgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2lhbS10ZXN0JyldKTtcblxuICAvLyBSb3VnaGx5IGNoZWNrIGZvciBhIHRhYmxlIGxpa2UgdGhpczpcbiAgLy9cbiAgLy8g4pSM4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSALeKUgOKUgOKUrOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUkFxuICAvLyDilIIgICDilIIgUmVzb3VyY2UgICAgICAgIOKUgiBFZmZlY3Qg4pSCIEFjdGlvbiAgICAgICAgIOKUgiBQcmluY2lwYWwgICAgICAgICAgICAgICAgICAgICDilIIgQ29uZGl0aW9uIOKUglxuICAvLyDilJzilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilKRcbiAgLy8g4pSCICsg4pSCICR7U29tZVJvbGUuQXJufSDilIIgQWxsb3cgIOKUgiBzdHM6QXNzdW1lUm9sZSDilIIgU2VydmljZTplYzIuYW1hem9uYXdzLmNvbSAgICAg4pSCICAgICAgICAgICDilIJcbiAgLy8g4pSU4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSYXG5cbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCcke1NvbWVSb2xlLkFybn0nKTtcbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCdzdHM6QXNzdW1lUm9sZScpO1xuICBleHBlY3Qob3V0cHV0KS50b0NvbnRhaW4oJ2VjMi5hbWF6b25hd3MuY29tJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZmFzdCBkZXBsb3knLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gd2UgYXJlIHVzaW5nIGEgc3RhY2sgd2l0aCBhIG5lc3RlZCBzdGFjayBiZWNhdXNlIENGTiB3aWxsIGFsd2F5cyBhdHRlbXB0IHRvXG4gIC8vIHVwZGF0ZSBhIG5lc3RlZCBzdGFjaywgd2hpY2ggd2lsbCBhbGxvdyB1cyB0byB2ZXJpZnkgdGhhdCB1cGRhdGVzIGFyZSBhY3R1YWxseVxuICAvLyBza2lwcGVkIHVubGVzcyAtLWZvcmNlIGlzIHNwZWNpZmllZC5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IGNhcHR1cmVTdGRlcnI6IGZhbHNlIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQxID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzYW1lIHN0YWNrIGFnYWluLCB0aGVyZSBzaG91bGQgYmUgbm8gbmV3IGNoYW5nZSBzZXQgY3JlYXRlZFxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snKTtcbiAgY29uc3QgY2hhbmdlU2V0MiA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCkudG9FcXVhbChjaGFuZ2VTZXQxLkNoYW5nZVNldElkKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIGFnYWluIHdpdGggLS1mb3JjZSwgbm93IHdlIHNob3VsZCBjcmVhdGUgYSBjaGFuZ2VzZXRcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3dpdGgtbmVzdGVkLXN0YWNrJywgeyBvcHRpb25zOiBbJy0tZm9yY2UnXSB9KTtcbiAgY29uc3QgY2hhbmdlU2V0MyA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0My5DaGFuZ2VTZXRJZCkubm90LnRvRXF1YWwoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzdGFjayBhZ2FpbiB3aXRoIHRhZ3MsIGV4cGVjdGVkIHRvIGNyZWF0ZSBhIG5ldyBjaGFuZ2VzZXRcbiAgLy8gZXZlbiB0aG91Z2ggdGhlIHJlc291cmNlcyBkaWRuJ3QgY2hhbmdlLlxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IG9wdGlvbnM6IFsnLS10YWdzJywgJ2tleT12YWx1ZSddIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQ0ID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG4gIGV4cGVjdChjaGFuZ2VTZXQ0LkNoYW5nZVNldElkKS5ub3QudG9FcXVhbChjaGFuZ2VTZXQzLkNoYW5nZVNldElkKTtcblxuICBhc3luYyBmdW5jdGlvbiBnZXRMYXRlc3RDaGFuZ2VTZXQoKSB7XG4gICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7IFN0YWNrTmFtZTogc3RhY2tBcm4gfSk7XG4gICAgaWYgKCFyZXNwb25zZS5TdGFja3M/LlswXSkgeyB0aHJvdyBuZXcgRXJyb3IoJ0RpZCBub3QgZ2V0IGEgQ2hhbmdlU2V0IGF0IGFsbCcpOyB9XG4gICAgZml4dHVyZS5sb2coYEZvdW5kIENoYW5nZSBTZXQgJHtyZXNwb25zZS5TdGFja3M/LlswXS5DaGFuZ2VTZXRJZH1gKTtcbiAgICByZXR1cm4gcmVzcG9uc2UuU3RhY2tzPy5bMF07XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdmYWlsZWQgZGVwbG95IGRvZXMgbm90IGhhbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gdGhpcyB3aWxsIGhhbmcgaWYgd2UgaW50cm9kdWNlIGh0dHBzOi8vZ2l0aHViLmNvbS9hd3MvYXdzLWNkay9pc3N1ZXMvNjQwMyBhZ2Fpbi5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrRGVwbG95KCdmYWlsZWQnKSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NhbiBzdGlsbCBsb2FkIG9sZCBhc3NlbWJsaWVzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGN4QXNtRGlyID0gcGF0aC5qb2luKG9zLnRtcGRpcigpLCAnY2RrLWludGVnLWN4Jyk7XG5cbiAgY29uc3QgdGVzdEFzc2VtYmxpZXNEaXJlY3RvcnkgPSBwYXRoLmpvaW4oX19kaXJuYW1lLCAnY2xvdWQtYXNzZW1ibGllcycpO1xuICBmb3IgKGNvbnN0IGFzbWRpciBvZiBhd2FpdCBsaXN0Q2hpbGREaXJzKHRlc3RBc3NlbWJsaWVzRGlyZWN0b3J5KSkge1xuICAgIGZpeHR1cmUubG9nKGBBU1NFTUJMWSAke2FzbWRpcn1gKTtcbiAgICBhd2FpdCBjbG9uZURpcmVjdG9yeShhc21kaXIsIGN4QXNtRGlyKTtcblxuICAgIC8vIFNvbWUgZmlsZXMgaW4gdGhlIGFzbSBkaXJlY3RvcnkgdGhhdCBoYXZlIGEgLmpzIGV4dGVuc2lvbiBhcmVcbiAgICAvLyBhY3R1YWxseSB0cmVhdGVkIGFzIHRlbXBsYXRlcy4gRXZhbHVhdGUgdGhlbSB1c2luZyBOb2RlSlMuXG4gICAgY29uc3QgdGVtcGxhdGVzID0gYXdhaXQgbGlzdENoaWxkcmVuKGN4QXNtRGlyLCBmdWxsUGF0aCA9PiBQcm9taXNlLnJlc29sdmUoZnVsbFBhdGguZW5kc1dpdGgoJy5qcycpKSk7XG4gICAgZm9yIChjb25zdCB0ZW1wbGF0ZSBvZiB0ZW1wbGF0ZXMpIHtcbiAgICAgIGNvbnN0IHRhcmdldE5hbWUgPSB0ZW1wbGF0ZS5yZXBsYWNlKC8uanMkLywgJycpO1xuICAgICAgYXdhaXQgc2hlbGwoW3Byb2Nlc3MuZXhlY1BhdGgsIHRlbXBsYXRlLCAnPicsIHRhcmdldE5hbWVdLCB7XG4gICAgICAgIGN3ZDogY3hBc21EaXIsXG4gICAgICAgIG91dHB1dDogZml4dHVyZS5vdXRwdXQsXG4gICAgICAgIG1vZEVudjoge1xuICAgICAgICAgIFRFU1RfQUNDT1VOVDogYXdhaXQgZml4dHVyZS5hd3MuYWNjb3VudCgpLFxuICAgICAgICAgIFRFU1RfUkVHSU9OOiBmaXh0dXJlLmF3cy5yZWdpb24sXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9XG5cbiAgICAvLyBVc2UgdGhpcyBkaXJlY3RvcnkgYXMgYSBDbG91ZCBBc3NlbWJseVxuICAgIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuY2RrKFtcbiAgICAgICctLWFwcCcsIGN4QXNtRGlyLFxuICAgICAgJy12JyxcbiAgICAgICdzeW50aCcsXG4gICAgXSk7XG5cbiAgICAvLyBBc3NlcnQgdGhhdCB0aGVyZSB3YXMgbm8gcHJvdmlkZXJFcnJvciBpbiBDREsncyBzdGRlcnJcbiAgICAvLyBCZWNhdXNlIHdlIHJlbHkgb24gdGhlIGFwcC9mcmFtZXdvcmsgdG8gYWN0dWFsbHkgZXJyb3IgaW4gY2FzZSB0aGVcbiAgICAvLyBwcm92aWRlciBmYWlscywgd2UgaW5zcGVjdCB0aGUgbG9ncyBoZXJlLlxuICAgIGV4cGVjdChvdXRwdXQpLm5vdC50b0NvbnRhaW4oJyRwcm92aWRlckVycm9yJyk7XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdnZW5lcmF0aW5nIGFuZCBsb2FkaW5nIGFzc2VtYmx5Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGFzbU91dHB1dERpciA9IGAke2ZpeHR1cmUuaW50ZWdUZXN0RGlyfS1jZGstaW50ZWctYXNtYDtcbiAgYXdhaXQgZml4dHVyZS5zaGVsbChbJ3JtJywgJy1yZicsIGFzbU91dHB1dERpcl0pO1xuXG4gIC8vIFN5bnRoZXNpemUgYSBDbG91ZCBBc3NlbWJseSB0b3RoZSBkZWZhdWx0IGRpcmVjdG9yeSAoY2RrLm91dCkgYW5kIGEgc3BlY2lmaWMgZGlyZWN0b3J5LlxuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJ10pO1xuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJywgJy0tb3V0cHV0JywgYXNtT3V0cHV0RGlyXSk7XG5cbiAgLy8gY2RrLm91dCBpbiB0aGUgY3VycmVudCBkaXJlY3RvcnkgYW5kIHRoZSBpbmRpY2F0ZWQgLS1vdXRwdXQgc2hvdWxkIGJlIHRoZSBzYW1lXG4gIGF3YWl0IGZpeHR1cmUuc2hlbGwoWydkaWZmJywgJ2Nkay5vdXQnLCBhc21PdXRwdXREaXJdKTtcblxuICAvLyBDaGVjayB0aGF0IHdlIGNhbiAnbHMnIHRoZSBzeW50aGVzaXplZCBhc20uXG4gIC8vIENoYW5nZSB0byBzb21lIHJhbmRvbSBkaXJlY3RvcnkgdG8gbWFrZSBzdXJlIHdlJ3JlIG5vdCBhY2NpZGVudGFsbHkgbG9hZGluZyBjZGsuanNvblxuICBjb25zdCBsaXN0ID0gYXdhaXQgZml4dHVyZS5jZGsoWyctLWFwcCcsIGFzbU91dHB1dERpciwgJ2xzJ10sIHsgY3dkOiBvcy50bXBkaXIoKSB9KTtcbiAgLy8gU2FtZSBzdGFja3Mgd2Uga25vdyBhcmUgaW4gdGhlIGFwcFxuICBleHBlY3QobGlzdCkudG9Db250YWluKGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1sYW1iZGFgKTtcbiAgZXhwZWN0KGxpc3QpLnRvQ29udGFpbihgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0xYCk7XG4gIGV4cGVjdChsaXN0KS50b0NvbnRhaW4oYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMmApO1xuXG4gIC8vIENoZWNrIHRoYXQgd2UgY2FuIHVzZSAnLicgYW5kIGp1c3Qgc3ludGggLHRoZSBnZW5lcmF0ZWQgYXNtXG4gIGNvbnN0IHN0YWNrVGVtcGxhdGUgPSBhd2FpdCBmaXh0dXJlLmNkayhbJy0tYXBwJywgJy4nLCAnc3ludGgnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMicpXSwge1xuICAgIGN3ZDogYXNtT3V0cHV0RGlyLFxuICB9KTtcbiAgZXhwZWN0KHN0YWNrVGVtcGxhdGUpLnRvQ29udGFpbigndG9waWMxNTJEODRBMzcnKTtcblxuICAvLyBEZXBsb3kgYSBMYW1iZGEgZnJvbSB0aGUgY29waWVkIGFzbVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnbGFtYmRhJywgeyBvcHRpb25zOiBbJy1hJywgJy4nXSwgY3dkOiBhc21PdXRwdXREaXIgfSk7XG5cbiAgLy8gUmVtb3ZlIChyZW5hbWUpIHRoZSBvcmlnaW5hbCBjdXN0b20gZG9ja2VyIGZpbGUgdGhhdCB3YXMgdXNlZCBkdXJpbmcgc3ludGguXG4gIC8vIHRoaXMgdmVyaWZpZXMgdGhhdCB0aGUgYXNzZW1seSBoYXMgYSBjb3B5IG9mIGl0IGFuZCB0aGF0IHRoZSBtYW5pZmVzdCB1c2VzXG4gIC8vIHJlbGF0aXZlIHBhdGhzIHRvIHJlZmVyZW5jZSB0byBpdC5cbiAgY29uc3QgY3VzdG9tRG9ja2VyRmlsZSA9IHBhdGguam9pbihmaXh0dXJlLmludGVnVGVzdERpciwgJ2RvY2tlcicsICdEb2NrZXJmaWxlLkN1c3RvbScpO1xuICBhd2FpdCBmcy5yZW5hbWUoY3VzdG9tRG9ja2VyRmlsZSwgYCR7Y3VzdG9tRG9ja2VyRmlsZX1+YCk7XG4gIHRyeSB7XG5cbiAgICAvLyBkZXBsb3kgYSBkb2NrZXIgaW1hZ2Ugd2l0aCBjdXN0b20gZmlsZSB3aXRob3V0IHN5bnRoICh1c2VzIGFzc2V0cylcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZG9ja2VyLXdpdGgtY3VzdG9tLWZpbGUnLCB7IG9wdGlvbnM6IFsnLWEnLCAnLiddLCBjd2Q6IGFzbU91dHB1dERpciB9KTtcblxuICB9IGZpbmFsbHkge1xuICAgIC8vIFJlbmFtZSBiYWNrIHRvIHJlc3RvcmUgZml4dHVyZSB0byBvcmlnaW5hbCBzdGF0ZVxuICAgIGF3YWl0IGZzLnJlbmFtZShgJHtjdXN0b21Eb2NrZXJGaWxlfX5gLCBjdXN0b21Eb2NrZXJGaWxlKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ3RlbXBsYXRlcyBvbiBkaXNrIGNvbnRhaW4gbWV0YWRhdGEgcmVzb3VyY2UsIGFsc28gaW4gbmVzdGVkIGFzc2VtYmxpZXMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gU3ludGggZmlyc3QsIGFuZCBzd2l0Y2ggb24gdmVyc2lvbiByZXBvcnRpbmcgYmVjYXVzZSBjZGsuanNvbiBpcyBkaXNhYmxpbmcgaXRcbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsICctLXZlcnNpb24tcmVwb3J0aW5nPXRydWUnXSk7XG5cbiAgLy8gTG9hZCB0ZW1wbGF0ZSBmcm9tIGRpc2sgZnJvbSByb290IGFzc2VtYmx5XG4gIGNvbnN0IHRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvKi1sYW1iZGEudGVtcGxhdGUuanNvbiddKTtcblxuICBleHBlY3QoSlNPTi5wYXJzZSh0ZW1wbGF0ZUNvbnRlbnRzKS5SZXNvdXJjZXMuQ0RLTWV0YWRhdGEpLnRvQmVUcnV0aHkoKTtcblxuICAvLyBMb2FkIHRlbXBsYXRlIGZyb20gbmVzdGVkIGFzc2VtYmx5XG4gIGNvbnN0IG5lc3RlZFRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvYXNzZW1ibHktKi1zdGFnZS8qLXN0YWdlLVN0YWNrSW5TdGFnZS50ZW1wbGF0ZS5qc29uJ10pO1xuXG4gIGV4cGVjdChKU09OLnBhcnNlKG5lc3RlZFRlbXBsYXRlQ29udGVudHMpLlJlc291cmNlcy5DREtNZXRhZGF0YSkudG9CZVRydXRoeSgpO1xufSkpO1xuXG5hc3luYyBmdW5jdGlvbiBsaXN0Q2hpbGRyZW4ocGFyZW50OiBzdHJpbmcsIHByZWQ6ICh4OiBzdHJpbmcpID0+IFByb21pc2U8Ym9vbGVhbj4pIHtcbiAgY29uc3QgcmV0ID0gbmV3IEFycmF5PHN0cmluZz4oKTtcbiAgZm9yIChjb25zdCBjaGlsZCBvZiBhd2FpdCBmcy5yZWFkZGlyKHBhcmVudCwgeyBlbmNvZGluZzogJ3V0Zi04JyB9KSkge1xuICAgIGNvbnN0IGZ1bGxQYXRoID0gcGF0aC5qb2luKHBhcmVudCwgY2hpbGQudG9TdHJpbmcoKSk7XG4gICAgaWYgKGF3YWl0IHByZWQoZnVsbFBhdGgpKSB7XG4gICAgICByZXQucHVzaChmdWxsUGF0aCk7XG4gICAgfVxuICB9XG4gIHJldHVybiByZXQ7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGxpc3RDaGlsZERpcnMocGFyZW50OiBzdHJpbmcpIHtcbiAgcmV0dXJuIGxpc3RDaGlsZHJlbihwYXJlbnQsIGFzeW5jIChmdWxsUGF0aDogc3RyaW5nKSA9PiAoYXdhaXQgZnMuc3RhdChmdWxsUGF0aCkpLmlzRGlyZWN0b3J5KCkpO1xufVxuXG5hc3luYyBmdW5jdGlvbiByZWFkVGVtcGxhdGUoZml4dHVyZTogVGVzdEZpeHR1cmUsIHN0YWNrTmFtZTogc3RyaW5nKTogUHJvbWlzZTxhbnk+IHtcbiAgY29uc3QgZnVsbFN0YWNrTmFtZSA9IGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFja05hbWUpO1xuICBjb25zdCB0ZW1wbGF0ZVBhdGggPSBwYXRoLmpvaW4oZml4dHVyZS5pbnRlZ1Rlc3REaXIsICdjZGsub3V0JywgYCR7ZnVsbFN0YWNrTmFtZX0udGVtcGxhdGUuanNvbmApO1xuICByZXR1cm4gSlNPTi5wYXJzZSgoYXdhaXQgZnMucmVhZEZpbGUodGVtcGxhdGVQYXRoLCB7IGVuY29kaW5nOiAndXRmLTgnIH0pKS50b1N0cmluZygpKTtcbn1cbiJdfQ== \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.130.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.130.0/NOTES.md new file mode 100644 index 000000000..5e17983dd --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.130.0/NOTES.md @@ -0,0 +1,12 @@ +--------------------------------------- + +On november 2nd 2021, lambda started deprecating the nodejs10.x runtime. This meant we can no longer create functions with this runtime. +Our integration tests use this runtime for one of its stacks. + +This patch brings https://github.com/aws/aws-cdk/pull/17282 into the regression suite. + +---------------------------------------- + +Needs to disable an existing test due to change in bootstrap of integration tests. + +This patch brings https://github.com/aws/aws-cdk/pull/17337 into the regression suite. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.130.0/app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.130.0/app/app.js new file mode 100644 index 000000000..cf63969d8 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.130.0/app/app.js @@ -0,0 +1,378 @@ +const path = require('path'); + +var constructs = require('constructs'); +if (process.env.PACKAGE_LAYOUT_VERSION === '1') { + var cdk = require('@aws-cdk/core'); + var ec2 = require('@aws-cdk/aws-ec2'); + var ssm = require('@aws-cdk/aws-ssm'); + var iam = require('@aws-cdk/aws-iam'); + var sns = require('@aws-cdk/aws-sns'); + var lambda = require('@aws-cdk/aws-lambda'); + var docker = require('@aws-cdk/aws-ecr-assets'); +} else { + var cdk = require('aws-cdk-lib'); + var { + aws_ec2: ec2, + aws_ssm: ssm, + aws_iam: iam, + aws_sns: sns, + aws_lambda: lambda, + aws_ecr_assets: docker + } = require('aws-cdk-lib'); +} + +const { Annotations } = cdk; +const { StackWithNestedStack, StackWithNestedStackUsingParameters } = require('./nested-stack'); + +const stackPrefix = process.env.STACK_NAME_PREFIX; +if (!stackPrefix) { + throw new Error(`the STACK_NAME_PREFIX environment variable is required`); +} + +class MyStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + new sns.Topic(this, 'topic'); + + if (cdk.AvailabilityZoneProvider) { // <= 0.34.0 + new cdk.AvailabilityZoneProvider(this).availabilityZones; + } else if (cdk.Context) { // <= 0.35.0 + cdk.Context.getAvailabilityZones(this); + } else { + this.availabilityZones; + } + + const parameterName = '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2'; + getSsmParameterValue(this, parameterName); + } +} + +function getSsmParameterValue(scope, parameterName) { + return ssm.StringParameter.valueFromLookup(scope, parameterName); +} + +class YourStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + new sns.Topic(this, 'topic1'); + new sns.Topic(this, 'topic2'); + } +} + +class StackUsingContext extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + new cdk.CfnResource(this, 'Handle', { + type: 'AWS::CloudFormation::WaitConditionHandle' + }); + + new cdk.CfnOutput(this, 'Output', { + value: this.availabilityZones, + }); + } +} + +class ParameterStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new sns.Topic(this, 'TopicParameter', { + topicName: new cdk.CfnParameter(this, 'TopicNameParam').valueAsString + }); + } +} + +class OtherParameterStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new sns.Topic(this, 'TopicParameter', { + topicName: new cdk.CfnParameter(this, 'OtherTopicNameParam').valueAsString + }); + } +} + +class MultiParameterStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new sns.Topic(this, 'TopicParameter', { + displayName: new cdk.CfnParameter(this, 'DisplayNameParam').valueAsString + }); + new sns.Topic(this, 'OtherTopicParameter', { + displayName: new cdk.CfnParameter(this, 'OtherDisplayNameParam').valueAsString + }); + } +} + +class OutputsStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const topic = new sns.Topic(this, 'MyOutput', { + topicName: `${cdk.Stack.of(this).stackName}MyTopic` + }); + + new cdk.CfnOutput(this, 'TopicName', { + value: topic.topicName + }) + } +} + +class AnotherOutputsStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const topic = new sns.Topic(this, 'MyOtherOutput', { + topicName: `${cdk.Stack.of(this).stackName}MyOtherTopic` + }); + + new cdk.CfnOutput(this, 'TopicName', { + value: topic.topicName + }); + } +} + +class IamStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new iam.Role(this, 'SomeRole', { + assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com') + }); + } +} + +class ProvidingStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + this.topic = new sns.Topic(this, 'BogusTopic'); // Some filler + } +} + +class StackWithError extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + this.topic = new sns.Topic(this, 'BogusTopic'); // Some filler + Annotations.of(this).addError('This is an error'); + } +} + +class StageWithError extends cdk.Stage { + constructor(parent, id, props) { + super(parent, id, props); + + new StackWithError(this, 'Stack'); + } +} + +class ConsumingStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new sns.Topic(this, 'BogusTopic'); // Some filler + new cdk.CfnOutput(this, 'IConsumedSomething', { value: props.providingStack.topic.topicArn }); + } +} + +class MissingSSMParameterStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const parameterName = constructs.Node.of(this).tryGetContext('test:ssm-parameter-name'); + if (parameterName) { + const param = getSsmParameterValue(this, parameterName); + new iam.Role(this, 'PhonyRole', { assumedBy: new iam.AccountPrincipal(param) }); + } + } +} + +class LambdaStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const fn = new lambda.Function(this, 'my-function', { + code: lambda.Code.asset(path.join(__dirname, 'lambda')), + runtime: lambda.Runtime.NODEJS_LATEST, + handler: 'index.handler' + }); + + new cdk.CfnOutput(this, 'FunctionArn', { value: fn.functionArn }); + } +} + +class DockerStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new docker.DockerImageAsset(this, 'image', { + directory: path.join(__dirname, 'docker') + }); + + // Add at least a single resource (WaitConditionHandle), otherwise this stack will never + // be deployed (and its assets never built) + new cdk.CfnResource(this, 'Handle', { + type: 'AWS::CloudFormation::WaitConditionHandle' + }); + } +} + +class DockerStackWithCustomFile extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + new docker.DockerImageAsset(this, 'image', { + directory: path.join(__dirname, 'docker'), + file: 'Dockerfile.Custom' + }); + + // Add at least a single resource (WaitConditionHandle), otherwise this stack will never + // be deployed (and its assets never built) + new cdk.CfnResource(this, 'Handle', { + type: 'AWS::CloudFormation::WaitConditionHandle' + }); + } +} + +class FailedStack extends cdk.Stack { + + constructor(parent, id, props) { + super(parent, id, props); + + // fails on 'Property PolicyDocument cannot be empty'. + new cdk.CfnResource(this, 'EmptyPolicy', { + type: 'AWS::IAM::Policy' + }) + + } + +} + +const VPC_TAG_NAME = 'custom-tag'; +const VPC_TAG_VALUE = `${stackPrefix}-bazinga!`; + +class DefineVpcStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + const vpc = new ec2.Vpc(this, 'VPC', { + maxAzs: 1, + }) + cdk.Aspects.of(vpc).add(new cdk.Tag(VPC_TAG_NAME, VPC_TAG_VALUE)); + } +} + +class ImportVpcStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + ec2.Vpc.fromLookup(this, 'DefaultVPC', { isDefault: true }); + ec2.Vpc.fromLookup(this, 'ByTag', { tags: { [VPC_TAG_NAME]: VPC_TAG_VALUE } }); + } +} + +class ConditionalResourceStack extends cdk.Stack { + constructor(parent, id, props) { + super(parent, id, props); + + if (!process.env.NO_RESOURCE) { + new iam.User(this, 'User'); + } + } +} + +class SomeStage extends cdk.Stage { + constructor(parent, id, props) { + super(parent, id, props); + + new YourStack(this, 'StackInStage'); + } +} + +class StageUsingContext extends cdk.Stage { + constructor(parent, id, props) { + super(parent, id, props); + + new StackUsingContext(this, 'StackInStage'); + } +} + +const app = new cdk.App(); + +const defaultEnv = { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION +}; + +// Sometimes we don't want to synthesize all stacks because it will impact the results +const stackSet = process.env.INTEG_STACK_SET || 'default'; + +switch (stackSet) { + case 'default': + // Deploy all does a wildcard ${stackPrefix}-test-* + new MyStack(app, `${stackPrefix}-test-1`, { env: defaultEnv }); + new YourStack(app, `${stackPrefix}-test-2`); + // Deploy wildcard with parameters does ${stackPrefix}-param-test-* + new ParameterStack(app, `${stackPrefix}-param-test-1`); + new OtherParameterStack(app, `${stackPrefix}-param-test-2`); + // Deploy stack with multiple parameters + new MultiParameterStack(app, `${stackPrefix}-param-test-3`); + // Deploy stack with outputs does ${stackPrefix}-outputs-test-* + new OutputsStack(app, `${stackPrefix}-outputs-test-1`); + new AnotherOutputsStack(app, `${stackPrefix}-outputs-test-2`); + // Not included in wildcard + new IamStack(app, `${stackPrefix}-iam-test`, { env: defaultEnv }); + const providing = new ProvidingStack(app, `${stackPrefix}-order-providing`); + new ConsumingStack(app, `${stackPrefix}-order-consuming`, { providingStack: providing }); + + new MissingSSMParameterStack(app, `${stackPrefix}-missing-ssm-parameter`, { env: defaultEnv }); + + new LambdaStack(app, `${stackPrefix}-lambda`); + new DockerStack(app, `${stackPrefix}-docker`); + new DockerStackWithCustomFile(app, `${stackPrefix}-docker-with-custom-file`); + new FailedStack(app, `${stackPrefix}-failed`) + + if (process.env.ENABLE_VPC_TESTING) { // Gating so we don't do context fetching unless that's what we are here for + const env = { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }; + if (process.env.ENABLE_VPC_TESTING === 'DEFINE') + new DefineVpcStack(app, `${stackPrefix}-define-vpc`, { env }); + if (process.env.ENABLE_VPC_TESTING === 'IMPORT') + new ImportVpcStack(app, `${stackPrefix}-import-vpc`, { env }); + } + + new ConditionalResourceStack(app, `${stackPrefix}-conditional-resource`) + + new StackWithNestedStack(app, `${stackPrefix}-with-nested-stack`); + new StackWithNestedStackUsingParameters(app, `${stackPrefix}-with-nested-stack-using-parameters`); + + new YourStack(app, `${stackPrefix}-termination-protection`, { + terminationProtection: process.env.TERMINATION_PROTECTION !== 'FALSE' ? true : false, + }); + + new SomeStage(app, `${stackPrefix}-stage`); + break; + + case 'stage-using-context': + // Cannot be combined with other test stacks, because we use this to test + // that stage context is propagated up and causes synth to fail when combined + // with '--no-lookups'. + + // Needs a dummy stack at the top level because the CLI will fail otherwise + new YourStack(app, `${stackPrefix}-toplevel`, { env: defaultEnv }); + new StageUsingContext(app, `${stackPrefix}-stage-using-context`, { + env: defaultEnv, + }); + break; + + case 'stage-with-errors': + const stage = new StageWithError(app, `${stackPrefix}-stage-with-errors`); + stage.synth({ validateOnSynthesis: true }); + break; + + default: + throw new Error(`Unrecognized INTEG_STACK_SET: '${stackSet}'`); +} + +app.synth(); diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.130.0/bootstrapping.integtest.js b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.130.0/bootstrapping.integtest.js new file mode 100644 index 000000000..33881210b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.130.0/bootstrapping.integtest.js @@ -0,0 +1,220 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs = require("fs"); +const path = require("path"); +const cdk_1 = require("../helpers/cdk"); +const test_helpers_1 = require("../helpers/test-helpers"); +const timeout = (process.env.CODEBUILD_BUILD_ID ?? process.env.GITHUB_RUN_ID) ? // if the process is running in CodeBuild + 3600000 : // 1 hour + 600000; // 10 minutes +jest.setTimeout(timeout); +process.stdout.write(`bootstrapping.integtest.ts: Setting jest time out to ${timeout} ms`); +test_helpers_1.integTest('can bootstrap without execution', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const bootstrapStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapLegacy({ + toolkitStackName: bootstrapStackName, + noExecute: true, + }); + const resp = await fixture.aws.cloudFormation('describeStacks', { + StackName: bootstrapStackName, + }); + expect((_a = resp.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); +})); +test_helpers_1.integTest('upgrade legacy bootstrap stack to new bootstrap stack while in use', cdk_1.withDefaultFixture(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + const legacyBootstrapBucketName = `aws-cdk-bootstrap-integ-test-legacy-bckt-${cdk_1.randomString()}`; + const newBootstrapBucketName = `aws-cdk-bootstrap-integ-test-v2-bckt-${cdk_1.randomString()}`; + fixture.rememberToDeleteBucket(legacyBootstrapBucketName); // This one will leak + fixture.rememberToDeleteBucket(newBootstrapBucketName); // This one shouldn't leak if the test succeeds, but let's be safe in case it doesn't + // Legacy bootstrap + await fixture.cdkBootstrapLegacy({ + toolkitStackName: bootstrapStackName, + bootstrapBucketName: legacyBootstrapBucketName, + }); + // Deploy stack that uses file assets + await fixture.cdkDeploy('lambda', { + options: ['--toolkit-stack-name', bootstrapStackName], + }); + // Upgrade bootstrap stack to "new" style + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + bootstrapBucketName: newBootstrapBucketName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + // (Force) deploy stack again + // --force to bypass the check which says that the template hasn't changed. + await fixture.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--force', + ], + }); +})); +test_helpers_1.integTest('can and deploy if omitting execution policies', cdk_1.withDefaultFixture(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + }); + // Deploy stack that uses file assets + await fixture.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +})); +test_helpers_1.integTest('deploy new style synthesis to new style bootstrap', cdk_1.withDefaultFixture(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + // Deploy stack that uses file assets + await fixture.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +})); +test_helpers_1.integTest('deploy new style synthesis to new style bootstrap (with docker image)', cdk_1.withDefaultFixture(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + // Deploy stack that uses file assets + await fixture.cdkDeploy('docker', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +})); +test_helpers_1.integTest('deploy old style synthesis to new style bootstrap', cdk_1.withDefaultFixture(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + // Deploy stack that uses file assets + await fixture.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + ], + }); +})); +test_helpers_1.integTest('can create a legacy bootstrap stack with --public-access-block-configuration=false', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const bootstrapStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapLegacy({ + verbose: true, + toolkitStackName: bootstrapStackName, + publicAccessBlockConfiguration: false, + tags: 'Foo=Bar', + }); + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Tags).toEqual([ + { Key: 'Foo', Value: 'Bar' }, + ]); +})); +test_helpers_1.integTest('can create multiple legacy bootstrap stacks', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const bootstrapStackName1 = `${fixture.bootstrapStackName}-1`; + const bootstrapStackName2 = `${fixture.bootstrapStackName}-2`; + // deploy two toolkit stacks into the same environment (see #1416) + // one with tags + await fixture.cdkBootstrapLegacy({ + verbose: true, + toolkitStackName: bootstrapStackName1, + tags: 'Foo=Bar', + }); + await fixture.cdkBootstrapLegacy({ + verbose: true, + toolkitStackName: bootstrapStackName2, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName1 }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Tags).toEqual([ + { Key: 'Foo', Value: 'Bar' }, + ]); +})); +test_helpers_1.integTest('can dump the template, modify and use it to deploy a custom bootstrap stack', cdk_1.withDefaultFixture(async (fixture) => { + let template = await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: fixture.bootstrapStackName, + showTemplate: true, + cliOptions: { + captureStderr: false, + }, + }); + expect(template).toContain('BootstrapVersion:'); + template += '\n' + [ + ' TwiddleDee:', + ' Value: Template got twiddled', + ].join('\n'); + const filename = path.join(fixture.integTestDir, `${fixture.qualifier}-template.yaml`); + fs.writeFileSync(filename, template, { encoding: 'utf-8' }); + await fixture.cdkBootstrapModern({ + toolkitStackName: fixture.bootstrapStackName, + template: filename, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); +})); +test_helpers_1.integTest('switch on termination protection, switch is left alone on re-bootstrap', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const bootstrapStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + terminationProtection: true, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + force: true, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].EnableTerminationProtection).toEqual(true); +})); +test_helpers_1.integTest('add tags, left alone on re-bootstrap', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const bootstrapStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + tags: 'Foo=Bar', + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + force: true, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Tags).toEqual([ + { Key: 'Foo', Value: 'Bar' }, + ]); +})); +test_helpers_1.integTest('can deploy modern-synthesized stack even if bootstrap stack name is unknown', cdk_1.withDefaultFixture(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + // Deploy stack that uses file assets + await fixture.cdkDeploy('lambda', { + options: [ + // Explicity pass a name that's sure to not exist, otherwise the CLI might accidentally find a + // default bootstracp stack if that happens to be in the account already. + '--toolkit-stack-name', 'DefinitelyDoesNotExist', + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +})); +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYm9vdHN0cmFwcGluZy5pbnRlZ3Rlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJib290c3RyYXBwaW5nLmludGVndGVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHlCQUF5QjtBQUN6Qiw2QkFBNkI7QUFDN0Isd0NBQWtFO0FBQ2xFLDBEQUFvRDtBQUVwRCxNQUFNLE9BQU8sR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLGtCQUFrQixDQUFDLENBQUMsQ0FBQyx5Q0FBeUM7SUFDeEYsT0FBUyxDQUFDLENBQUMsQ0FBQyxTQUFTO0lBQ3JCLE1BQU8sQ0FBQyxDQUFDLGFBQWE7QUFDeEIsSUFBSSxDQUFDLFVBQVUsQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUN6QixPQUFPLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyx3REFBd0QsT0FBTyxLQUFLLENBQUMsQ0FBQztBQUUzRix3QkFBUyxDQUFDLGlDQUFpQyxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTs7SUFDaEYsTUFBTSxrQkFBa0IsR0FBRyxPQUFPLENBQUMsa0JBQWtCLENBQUM7SUFFdEQsTUFBTSxPQUFPLENBQUMsa0JBQWtCLENBQUM7UUFDL0IsZ0JBQWdCLEVBQUUsa0JBQWtCO1FBQ3BDLFNBQVMsRUFBRSxJQUFJO0tBQ2hCLENBQUMsQ0FBQztJQUVILE1BQU0sSUFBSSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUU7UUFDOUQsU0FBUyxFQUFFLGtCQUFrQjtLQUM5QixDQUFDLENBQUM7SUFFSCxNQUFNLE9BQUMsSUFBSSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDO0FBQ3JFLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLG9FQUFvRSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUs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QUFHLEVBQUUsS0FBSyxFQUFFLEtBQUssRUFBRSxLQUFLLEVBQUU7S0FDN0IsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsNkVBQTZFLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQzVILE1BQU0sa0JBQWtCLEdBQUcsT0FBTyxDQUFDLGtCQUFrQixDQUFDO0lBRXRELE1BQU0sT0FBTyxDQUFDLGtCQUFrQixDQUFDO1FBQy9CLGdCQUFnQixFQUFFLGtCQUFrQjtRQUNwQyxrQkFBa0IsRUFBRSw2Q0FBNkM7S0FDbEUsQ0FBQyxDQUFDO0lBRUgscUNBQXFDO0lBQ3JDLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxRQUFRLEVBQUU7UUFDaEMsT0FBTyxFQUFFO1lBQ1AsOEZBQThGO1lBQzlGLHlFQUF5RTtZQUN6RSxzQkFBc0IsRUFBRSx3QkFBd0I7WUFDaEQsV0FBVyxFQUFFLG9DQUFvQyxPQUFPLENBQUMsU0FBUyxFQUFFO1lBQ3BFLFdBQVcsRUFBRSx3Q0FBd0M7U0FDdEQ7S0FDRixDQUFDLENBQUM7QUFDTCxDQUFDLENBQUMsQ0FBQyxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgZnMgZnJvbSAnZnMnO1xuaW1wb3J0ICogYXMgcGF0aCBmcm9tICdwYXRoJztcbmltcG9ydCB7IHJhbmRvbVN0cmluZywgd2l0aERlZmF1bHRGaXh0dXJlIH0gZnJvbSAnLi4vaGVscGVycy9jZGsnO1xuaW1wb3J0IHsgaW50ZWdUZXN0IH0gZnJvbSAnLi4vaGVscGVycy90ZXN0LWhlbHBlcnMnO1xuXG5jb25zdCB0aW1lb3V0ID0gcHJvY2Vzcy5lbnYuQ09ERUJVSUxEX0JVSUxEX0lEID8gLy8gaWYgdGhlIHByb2Nlc3MgaXMgcnVubmluZyBpbiBDb2RlQnVpbGRcbiAgM182MDBfMDAwIDogLy8gMSBob3VyXG4gIDYwMF8wMDA7IC8vIDEwIG1pbnV0ZXNcbmplc3Quc2V0VGltZW91dCh0aW1lb3V0KTtcbnByb2Nlc3Muc3Rkb3V0LndyaXRlKGBib290c3RyYXBwaW5nLmludGVndGVzdC50czogU2V0dGluZyBqZXN0IHRpbWUgb3V0IHRvICR7dGltZW91dH0gbXNgKTtcblxuaW50ZWdUZXN0KCdjYW4gYm9vdHN0cmFwIHdpdGhvdXQgZXhlY3V0aW9uJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGJvb3RzdHJhcFN0YWNrTmFtZSA9IGZpeHR1cmUuYm9vdHN0cmFwU3RhY2tOYW1lO1xuXG4gIGF3YWl0IGZpeHR1cmUuY2RrQm9vdHN0cmFwTGVnYWN5KHtcbiAgICB0b29sa2l0U3RhY2tOYW1lOiBib290c3RyYXBTdGFja05hbWUsXG4gICAgbm9FeGVjdXRlOiB0cnVlLFxuICB9KTtcblxuICBjb25zdCByZXNwID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lLFxuICB9KTtcblxuICBleHBlY3QocmVzcC5TdGFja3M/LlswXS5TdGFja1N0YXR1cykudG9FcXVhbCgnUkVWSUVXX0lOX1BST0dSRVNTJyk7XG59KSk7XG5cbmludGVnVGVzdCgndXBncmFkZSBsZWdhY3kgYm9vdHN0cmFwIHN0YWNrIHRvIG5ldyBib290c3RyYXAgc3RhY2sgd2hpbGUgaW4gdXNlJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGJvb3RzdHJhcFN0YWNrTmFtZSA9IGZpeHR1cmUuYm9vdHN0cmFwU3RhY2tOYW1lO1xuXG4gIGNvbnN0IGxlZ2FjeUJvb3RzdHJhcEJ1Y2tldE5hbWUgPSBgYXdzLWNkay1ib290c3RyYXAtaW50ZWctdGVzdC1sZWdhY3ktYmNrdC0ke3JhbmRvbVN0cmluZygpfWA7XG4gIGNvbnN0IG5ld0Jvb3RzdHJhcEJ1Y2tldE5hbWUgPSBgYXdzLWNkay1ib290c3RyYXAtaW50ZWctdGVzdC12Mi1iY2t0LSR7cmFuZG9tU3RyaW5nKCl9YDtcbiAgZml4dHVyZS5yZW1lbWJlclRvRGVsZXRlQnVja2V0KGxlZ2FjeUJvb3RzdHJhcEJ1Y2tldE5hbWUpOyAvLyBUaGlzIG9uZSB3aWxsIGxlYWtcbiAgZml4dHVyZS5yZW1lbWJlclRvRGVsZXRlQnVja2V0KG5ld0Jvb3RzdHJhcEJ1Y2tldE5hbWUpOyAvLyBUaGlzIG9uZSBzaG91bGRuJ3QgbGVhayBpZiB0aGUgdGVzdCBzdWNjZWVkcywgYnV0IGxldCdzIGJlIHNhZmUgaW4gY2FzZSBpdCBkb2Vzbid0XG5cbiAgLy8gTGVnYWN5IGJvb3RzdHJhcFxuICBhd2FpdCBmaXh0dXJlLmNka0Jvb3RzdHJhcExlZ2FjeSh7XG4gICAgdG9vbGtpdFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgIGJvb3RzdHJhcEJ1Y2tldE5hbWU6IGxlZ2FjeUJvb3RzdHJhcEJ1Y2tldE5hbWUsXG4gIH0pO1xuXG4gIC8vIERlcGxveSBzdGFjayB0aGF0IHVzZXMgZmlsZSBhc3NldHNcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ2xhbWJkYScsIHtcbiAgICBvcHRpb25zOiBbJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lXSxcbiAgfSk7XG5cbiAgLy8gVXBncmFkZSBib290c3RyYXAgc3RhY2sgdG8gXCJuZXdcIiBzdHlsZVxuICBhd2FpdCBmaXh0dXJlLmNka0Jvb3RzdHJhcE1vZGVybih7XG4gICAgdG9vbGtpdFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgIGJvb3RzdHJhcEJ1Y2tldE5hbWU6IG5ld0Jvb3RzdHJhcEJ1Y2tldE5hbWUsXG4gICAgY2ZuRXhlY3V0aW9uUG9saWN5OiAnYXJuOmF3czppYW06OmF3czpwb2xpY3kvQWRtaW5pc3RyYXRvckFjY2VzcycsXG4gIH0pO1xuXG4gIC8vIChGb3JjZSkgZGVwbG95IHN0YWNrIGFnYWluXG4gIC8vIC0tZm9yY2UgdG8gYnlwYXNzIHRoZSBjaGVjayB3aGljaCBzYXlzIHRoYXQgdGhlIHRlbXBsYXRlIGhhc24ndCBjaGFuZ2VkLlxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnbGFtYmRhJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICAgICctLWZvcmNlJyxcbiAgICBdLFxuICB9KTtcbn0pKTtcblxuaW50ZWdUZXN0KCdjYW4gYW5kIGRlcGxveSBpZiBvbWl0dGluZyBleGVjdXRpb24gcG9saWNpZXMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lID0gZml4dHVyZS5ib290c3RyYXBTdGFja05hbWU7XG5cbiAgYXdhaXQgZml4dHVyZS5jZGtCb290c3RyYXBNb2Rlcm4oe1xuICAgIHRvb2xraXRTdGFja05hbWU6IGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgfSk7XG5cbiAgLy8gRGVwbG95IHN0YWNrIHRoYXQgdXNlcyBmaWxlIGFzc2V0c1xuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnbGFtYmRhJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICAgICctLWNvbnRleHQnLCBgQGF3cy1jZGsvY29yZTpib290c3RyYXBRdWFsaWZpZXI9JHtmaXh0dXJlLnF1YWxpZmllcn1gLFxuICAgICAgJy0tY29udGV4dCcsICdAYXdzLWNkay9jb3JlOm5ld1N0eWxlU3RhY2tTeW50aGVzaXM9MScsXG4gICAgXSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IG5ldyBzdHlsZSBzeW50aGVzaXMgdG8gbmV3IHN0eWxlIGJvb3RzdHJhcCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBib290c3RyYXBTdGFja05hbWUgPSBmaXh0dXJlLmJvb3RzdHJhcFN0YWNrTmFtZTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0Jvb3RzdHJhcE1vZGVybih7XG4gICAgdG9vbGtpdFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgIGNmbkV4ZWN1dGlvblBvbGljeTogJ2Fybjphd3M6aWFtOjphd3M6cG9saWN5L0FkbWluaXN0cmF0b3JBY2Nlc3MnLFxuICB9KTtcblxuICAvLyBEZXBsb3kgc3RhY2sgdGhhdCB1c2VzIGZpbGUgYXNzZXRzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdsYW1iZGEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgICAgJy0tY29udGV4dCcsIGBAYXdzLWNkay9jb3JlOmJvb3RzdHJhcFF1YWxpZmllcj0ke2ZpeHR1cmUucXVhbGlmaWVyfWAsXG4gICAgICAnLS1jb250ZXh0JywgJ0Bhd3MtY2RrL2NvcmU6bmV3U3R5bGVTdGFja1N5bnRoZXNpcz0xJyxcbiAgICBdLFxuICB9KTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgbmV3IHN0eWxlIHN5bnRoZXNpcyB0byBuZXcgc3R5bGUgYm9vdHN0cmFwICh3aXRoIGRvY2tlciBpbWFnZSknLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lID0gZml4dHVyZS5ib290c3RyYXBTdGFja05hbWU7XG5cbiAgYXdhaXQgZml4dHVyZS5jZGtCb290c3RyYXBNb2Rlcm4oe1xuICAgIHRvb2xraXRTdGFja05hbWU6IGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICBjZm5FeGVjdXRpb25Qb2xpY3k6ICdhcm46YXdzOmlhbTo6YXdzOnBvbGljeS9BZG1pbmlzdHJhdG9yQWNjZXNzJyxcbiAgfSk7XG5cbiAgLy8gRGVwbG95IHN0YWNrIHRoYXQgdXNlcyBmaWxlIGFzc2V0c1xuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZG9ja2VyJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICAgICctLWNvbnRleHQnLCBgQGF3cy1jZGsvY29yZTpib290c3RyYXBRdWFsaWZpZXI9JHtmaXh0dXJlLnF1YWxpZmllcn1gLFxuICAgICAgJy0tY29udGV4dCcsICdAYXdzLWNkay9jb3JlOm5ld1N0eWxlU3RhY2tTeW50aGVzaXM9MScsXG4gICAgXSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IG9sZCBzdHlsZSBzeW50aGVzaXMgdG8gbmV3IHN0eWxlIGJvb3RzdHJhcCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBib290c3RyYXBTdGFja05hbWUgPSBmaXh0dXJlLmJvb3RzdHJhcFN0YWNrTmFtZTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0Jvb3RzdHJhcE1vZGVybih7XG4gICAgdG9vbGtpdFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgIGNmbkV4ZWN1dGlvblBvbGljeTogJ2Fybjphd3M6aWFtOjphd3M6cG9saWN5L0FkbWluaXN0cmF0b3JBY2Nlc3MnLFxuICB9KTtcblxuICAvLyBEZXBsb3kgc3RhY2sgdGhhdCB1c2VzIGZpbGUgYXNzZXRzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdsYW1iZGEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgIF0sXG4gIH0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NhbiBjcmVhdGUgYSBsZWdhY3kgYm9vdHN0cmFwIHN0YWNrIHdpdGggLS1wdWJsaWMtYWNjZXNzLWJsb2NrLWNvbmZpZ3VyYXRpb249ZmFsc2UnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lID0gZml4dHVyZS5ib290c3RyYXBTdGFja05hbWU7XG5cbiAgYXdhaXQgZml4dHVyZS5jZGtCb290c3RyYXBMZWdhY3koe1xuICAgIHZlcmJvc2U6IHRydWUsXG4gICAgdG9vbGtpdFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgIHB1YmxpY0FjY2Vzc0Jsb2NrQ29uZmlndXJhdGlvbjogZmFsc2UsXG4gICAgdGFnczogJ0Zvbz1CYXInLFxuICB9KTtcblxuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBib290c3RyYXBTdGFja05hbWUgfSk7XG4gIGV4cGVjdChyZXNwb25zZS5TdGFja3M/LlswXS5UYWdzKS50b0VxdWFsKFtcbiAgICB7IEtleTogJ0ZvbycsIFZhbHVlOiAnQmFyJyB9LFxuICBdKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdjYW4gY3JlYXRlIG11bHRpcGxlIGxlZ2FjeSBib290c3RyYXAgc3RhY2tzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGJvb3RzdHJhcFN0YWNrTmFtZTEgPSBgJHtmaXh0dXJlLmJvb3RzdHJhcFN0YWNrTmFtZX0tMWA7XG4gIGNvbnN0IGJvb3RzdHJhcFN0YWNrTmFtZTIgPSBgJHtmaXh0dXJlLmJvb3RzdHJhcFN0YWNrTmFtZX0tMmA7XG5cbiAgLy8gZGVwbG95IHR3byB0b29sa2l0IHN0YWNrcyBpbnRvIHRoZSBzYW1lIGVudmlyb25tZW50IChzZWUgIzE0MTYpXG4gIC8vIG9uZSB3aXRoIHRhZ3NcbiAgYXdhaXQgZml4dHVyZS5jZGtCb290c3RyYXBMZWdhY3koe1xuICAgIHZlcmJvc2U6IHRydWUsXG4gICAgdG9vbGtpdFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lMSxcbiAgICB0YWdzOiAnRm9vPUJhcicsXG4gIH0pO1xuICBhd2FpdCBmaXh0dXJlLmNka0Jvb3RzdHJhcExlZ2FjeSh7XG4gICAgdmVyYm9zZTogdHJ1ZSxcbiAgICB0b29sa2l0U3RhY2tOYW1lOiBib290c3RyYXBTdGFja05hbWUyLFxuICB9KTtcblxuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBib290c3RyYXBTdGFja05hbWUxIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uVGFncykudG9FcXVhbChbXG4gICAgeyBLZXk6ICdGb28nLCBWYWx1ZTogJ0JhcicgfSxcbiAgXSk7XG59KSk7XG5cbmludGVnVGVzdCgnY2FuIGR1bXAgdGhlIHRlbXBsYXRlLCBtb2RpZnkgYW5kIHVzZSBpdCB0byBkZXBsb3kgYSBjdXN0b20gYm9vdHN0cmFwIHN0YWNrJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGxldCB0ZW1wbGF0ZSA9IGF3YWl0IGZpeHR1cmUuY2RrQm9vdHN0cmFwTW9kZXJuKHtcbiAgICAvLyB0b29sa2l0U3RhY2tOYW1lIGRvZXNuJ3QgbWF0dGVyIGZvciB0aGlzIHBhcnRpY3VsYXIgaW52b2NhdGlvblxuICAgIHRvb2xraXRTdGFja05hbWU6IGZpeHR1cmUuYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgIHNob3dUZW1wbGF0ZTogdHJ1ZSxcbiAgICBjbGlPcHRpb25zOiB7XG4gICAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgICB9LFxuICB9KTtcblxuICBleHBlY3QodGVtcGxhdGUpLnRvQ29udGFpbignQm9vdHN0cmFwVmVyc2lvbjonKTtcblxuICB0ZW1wbGF0ZSArPSAnXFxuJyArIFtcbiAgICAnICBUd2lkZGxlRGVlOicsXG4gICAgJyAgICBWYWx1ZTogVGVtcGxhdGUgZ290IHR3aWRkbGVkJyxcbiAgXS5qb2luKCdcXG4nKTtcblxuICBjb25zdCBmaWxlbmFtZSA9IHBhdGguam9pbihmaXh0dXJlLmludGVnVGVzdERpciwgYCR7Zml4dHVyZS5xdWFsaWZpZXJ9LXRlbXBsYXRlLnlhbWxgKTtcbiAgZnMud3JpdGVGaWxlU3luYyhmaWxlbmFtZSwgdGVtcGxhdGUsIHsgZW5jb2Rpbmc6ICd1dGYtOCcgfSk7XG4gIGF3YWl0IGZpeHR1cmUuY2RrQm9vdHN0cmFwTW9kZXJuKHtcbiAgICB0b29sa2l0U3RhY2tOYW1lOiBmaXh0dXJlLmJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICB0ZW1wbGF0ZTogZmlsZW5hbWUsXG4gICAgY2ZuRXhlY3V0aW9uUG9saWN5OiAnYXJuOmF3czppYW06OmF3czpwb2xpY3kvQWRtaW5pc3RyYXRvckFjY2VzcycsXG4gIH0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ3N3aXRjaCBvbiB0ZXJtaW5hdGlvbiBwcm90ZWN0aW9uLCBzd2l0Y2ggaXMgbGVmdCBhbG9uZSBvbiByZS1ib290c3RyYXAnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lID0gZml4dHVyZS5ib290c3RyYXBTdGFja05hbWU7XG5cbiAgYXdhaXQgZml4dHVyZS5jZGtCb290c3RyYXBNb2Rlcm4oe1xuICAgIHZlcmJvc2U6IHRydWUsXG4gICAgdG9vbGtpdFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgIHRlcm1pbmF0aW9uUHJvdGVjdGlvbjogdHJ1ZSxcbiAgICBjZm5FeGVjdXRpb25Qb2xpY3k6ICdhcm46YXdzOmlhbTo6YXdzOnBvbGljeS9BZG1pbmlzdHJhdG9yQWNjZXNzJyxcbiAgfSk7XG4gIGF3YWl0IGZpeHR1cmUuY2RrQm9vdHN0cmFwTW9kZXJuKHtcbiAgICB2ZXJib3NlOiB0cnVlLFxuICAgIHRvb2xraXRTdGFja05hbWU6IGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICBmb3JjZTogdHJ1ZSxcbiAgfSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7IFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uRW5hYmxlVGVybWluYXRpb25Qcm90ZWN0aW9uKS50b0VxdWFsKHRydWUpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2FkZCB0YWdzLCBsZWZ0IGFsb25lIG9uIHJlLWJvb3RzdHJhcCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBib290c3RyYXBTdGFja05hbWUgPSBmaXh0dXJlLmJvb3RzdHJhcFN0YWNrTmFtZTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0Jvb3RzdHJhcE1vZGVybih7XG4gICAgdmVyYm9zZTogdHJ1ZSxcbiAgICB0b29sa2l0U3RhY2tOYW1lOiBib290c3RyYXBTdGFja05hbWUsXG4gICAgdGFnczogJ0Zvbz1CYXInLFxuICAgIGNmbkV4ZWN1dGlvblBvbGljeTogJ2Fybjphd3M6aWFtOjphd3M6cG9saWN5L0FkbWluaXN0cmF0b3JBY2Nlc3MnLFxuICB9KTtcbiAgYXdhaXQgZml4dHVyZS5jZGtCb290c3RyYXBNb2Rlcm4oe1xuICAgIHZlcmJvc2U6IHRydWUsXG4gICAgdG9vbGtpdFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgIGZvcmNlOiB0cnVlLFxuICB9KTtcblxuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBib290c3RyYXBTdGFja05hbWUgfSk7XG4gIGV4cGVjdChyZXNwb25zZS5TdGFja3M/LlswXS5UYWdzKS50b0VxdWFsKFtcbiAgICB7IEtleTogJ0ZvbycsIFZhbHVlOiAnQmFyJyB9LFxuICBdKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdjYW4gZGVwbG95IG1vZGVybi1zeW50aGVzaXplZCBzdGFjayBldmVuIGlmIGJvb3RzdHJhcCBzdGFjayBuYW1lIGlzIHVua25vd24nLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lID0gZml4dHVyZS5ib290c3RyYXBTdGFja05hbWU7XG5cbiAgYXdhaXQgZml4dHVyZS5jZGtCb290c3RyYXBNb2Rlcm4oe1xuICAgIHRvb2xraXRTdGFja05hbWU6IGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICBjZm5FeGVjdXRpb25Qb2xpY3k6ICdhcm46YXdzOmlhbTo6YXdzOnBvbGljeS9BZG1pbmlzdHJhdG9yQWNjZXNzJyxcbiAgfSk7XG5cbiAgLy8gRGVwbG95IHN0YWNrIHRoYXQgdXNlcyBmaWxlIGFzc2V0c1xuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnbGFtYmRhJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgIC8vIEV4cGxpY2l0eSBwYXNzIGEgbmFtZSB0aGF0J3Mgc3VyZSB0byBub3QgZXhpc3QsIG90aGVyd2lzZSB0aGUgQ0xJIG1pZ2h0IGFjY2lkZW50YWxseSBmaW5kIGFcbiAgICAgIC8vIGRlZmF1bHQgYm9vdHN0cmFjcCBzdGFjayBpZiB0aGF0IGhhcHBlbnMgdG8gYmUgaW4gdGhlIGFjY291bnQgYWxyZWFkeS5cbiAgICAgICctLXRvb2xraXQtc3RhY2stbmFtZScsICdEZWZpbml0ZWx5RG9lc05vdEV4aXN0JyxcbiAgICAgICctLWNvbnRleHQnLCBgQGF3cy1jZGsvY29yZTpib290c3RyYXBRdWFsaWZpZXI9JHtmaXh0dXJlLnF1YWxpZmllcn1gLFxuICAgICAgJy0tY29udGV4dCcsICdAYXdzLWNkay9jb3JlOm5ld1N0eWxlU3RhY2tTeW50aGVzaXM9MScsXG4gICAgXSxcbiAgfSk7XG59KSk7XG4iXX0= \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.44.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.44.0/NOTES.md new file mode 100644 index 000000000..961813bc3 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.44.0/NOTES.md @@ -0,0 +1,18 @@ +Patch notes: + +- Replace `test.sh` since we removed the old test exclusion + mechanism, and the `cli.exclusions.js` file that the old `test.sh` + depended upon. + +- We removed the old asset-publishing role from the new bootstrap + stack, and split it into separate file- and docker-publishing roles. + Since 1.44.0 would still expect the old asset-publishing role, + its test would fail, so we disable it: + +``` +test.skip('deploy new style synthesis to new style bootstrap', async () => { +``` + +There is a better mechanism for skipping certain tests by using `skip-tests.txt`, +but that one is only available AFTER this release, so for this version we just replace +source files. diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.44.0/bootstrapping.integtest.js b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.44.0/bootstrapping.integtest.js new file mode 100644 index 000000000..d715afa92 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.44.0/bootstrapping.integtest.js @@ -0,0 +1,126 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const aws_helpers_1 = require("./aws-helpers"); +const cdk_helpers_1 = require("./cdk-helpers"); +jest.setTimeout(600000); +const QUALIFIER = randomString(); +beforeAll(async () => { + await cdk_helpers_1.prepareAppFixture(); +}); +beforeEach(async () => { + await cdk_helpers_1.cleanup(); +}); +afterEach(async () => { + await cdk_helpers_1.cleanup(); +}); +test('can bootstrap without execution', async () => { + var _a; + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--no-execute']); + const resp = await aws_helpers_1.cloudFormation('describeStacks', { + StackName: bootstrapStackName, + }); + expect((_a = resp.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); +}); +test('upgrade legacy bootstrap stack to new bootstrap stack while in use', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + const legacyBootstrapBucketName = `aws-cdk-bootstrap-integ-test-legacy-bckt-${randomString()}`; + const newBootstrapBucketName = `aws-cdk-bootstrap-integ-test-v2-bckt-${randomString()}`; + cdk_helpers_1.rememberToDeleteBucket(legacyBootstrapBucketName); // This one will leak + cdk_helpers_1.rememberToDeleteBucket(newBootstrapBucketName); // This one shouldn't leak if the test succeeds, but let's be safe in case it doesn't + // Legacy bootstrap + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--bootstrap-bucket-name', legacyBootstrapBucketName]); + // Deploy stack that uses file assets + await cdk_helpers_1.cdkDeploy('lambda', { + options: ['--toolkit-stack-name', bootstrapStackName], + }); + // Upgrade bootstrap stack to "new" style + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--bootstrap-bucket-name', newBootstrapBucketName, + '--qualifier', QUALIFIER], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + // (Force) deploy stack again + // --force to bypass the check which says that the template hasn't changed. + await cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--force', + ], + }); +}); +test.skip('deploy new style synthesis to new style bootstrap', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--qualifier', QUALIFIER, + '--cloudformation-execution-policies', 'arn:aws:iam::aws:policy/AdministratorAccess', + ], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + // Deploy stack that uses file assets + await cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${QUALIFIER}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +}); +test('deploy old style synthesis to new style bootstrap', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--qualifier', QUALIFIER, + '--cloudformation-execution-policies', 'arn:aws:iam::aws:policy/AdministratorAccess', + ], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + // Deploy stack that uses file assets + await cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + ], + }); +}); +test('deploying new style synthesis to old style bootstrap fails', async () => { + const bootstrapStackName = cdk_helpers_1.fullStackName('bootstrap-stack'); + await cdk_helpers_1.cdk(['bootstrap', '--toolkit-stack-name', bootstrapStackName]); + // Deploy stack that uses file assets, this fails because the bootstrap stack + // is version checked. + await expect(cdk_helpers_1.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + })).rejects.toThrow('exited with error'); +}); +test('can create multiple legacy bootstrap stacks', async () => { + var _a; + const bootstrapStackName1 = cdk_helpers_1.fullStackName('bootstrap-stack-1'); + const bootstrapStackName2 = cdk_helpers_1.fullStackName('bootstrap-stack-2'); + // deploy two toolkit stacks into the same environment (see #1416) + // one with tags + await cdk_helpers_1.cdk(['bootstrap', '-v', '--toolkit-stack-name', bootstrapStackName1, '--tags', 'Foo=Bar']); + await cdk_helpers_1.cdk(['bootstrap', '-v', '--toolkit-stack-name', bootstrapStackName2]); + const response = await aws_helpers_1.cloudFormation('describeStacks', { StackName: bootstrapStackName1 }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Tags).toEqual([ + { Key: 'Foo', Value: 'Bar' }, + ]); +}); +function randomString() { + // Crazy + return Math.random().toString(36).replace(/[^a-z0-9]+/g, ''); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYm9vdHN0cmFwcGluZy5pbnRlZ3Rlc3QuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJib290c3RyYXBwaW5nLmludGVndGVzdC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLCtDQUErQztBQUMvQywrQ0FBa0g7QUFFbEgsSUFBSSxDQUFDLFVBQVUsQ0FBQyxNQUFPLENBQUMsQ0FBQztBQUV6QixNQUFNLFNBQVMsR0FBRyxZQUFZLEVBQUUsQ0FBQztBQUVqQyxTQUFTLENBQUMsS0FBSyxJQUFJLEVBQUU7SUFDbkIsTUFBTSwrQkFBaUIsRUFBRSxDQUFDO0FBQzVCLENBQUMsQ0FBQyxDQUFDO0FBRUgsVUFBVSxDQUFDLEtBQUssSUFBSSxFQUFFO0lBQ3BCLE1BQU0scUJBQU8sRUFBRSxDQUFDO0FBQ2xCLENBQUMsQ0FBQyxDQUFDO0FBRUgsU0FBUyxDQUFDLEtBQUssSUFBSSxFQUFFO0lBQ25CLE1BQU0scUJBQU8sRUFBRSxDQUFDO0FBQ2xCLENBQUMsQ0FBQyxDQUFDO0FBRUgsSUFBSSxDQUFDLGlDQUFpQyxFQUFFLEtBQUssSUFBSSxFQUFFOztJQUNqRCxNQUFNLGtCQUFrQixHQUFHLDJCQUFhLENBQUMsaUJBQWlCLENBQUMsQ0FBQztJQUU1RCxNQUFNLGlCQUFHLENBQUMsQ0FBQyxXQUFXO1FBQ3BCLHNCQUFzQixFQUFFLGtCQUFrQjtRQUMxQyxjQUFjLENBQUMsQ0FBQyxDQUFDO0lBRW5CLE1BQU0sSUFBSSxHQUFHLE1BQU0sNEJBQWMsQ0FBQyxnQkFBZ0IsRUFBRTtRQUNsRCxTQUFTLEVBQUUsa0JBQWtCO0tBQzlCLENBQUMsQ0FBQztJQUVILE1BQU0sT0FBQyxJQUFJLENBQUMsTUFBTSwwQ0FBRyxDQUFDLEVBQUUsV0FBVyxDQUFDLENBQUMsT0FBTyxDQUFDLG9CQUFvQixDQUFDLENBQUM7QUFDckUsQ0FBQyxDQUFDLENBQUM7QUFFSCxJQUFJLENBQUMsb0VBQW9FLEVBQUUsS0FBSyxJQUFJLEVBQUU7SUFDcEYsTUFBTSxrQkFB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RSxDQUFDLENBQUM7QUFDL0QsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IGNsb3VkRm9ybWF0aW9uIH0gZnJvbSAnLi9hd3MtaGVscGVycyc7XG5pbXBvcnQgeyBjZGssIGNka0RlcGxveSwgY2xlYW51cCwgZnVsbFN0YWNrTmFtZSwgcHJlcGFyZUFwcEZpeHR1cmUsIHJlbWVtYmVyVG9EZWxldGVCdWNrZXQgfSBmcm9tICcuL2Nkay1oZWxwZXJzJztcblxuamVzdC5zZXRUaW1lb3V0KDYwMF8wMDApO1xuXG5jb25zdCBRVUFMSUZJRVIgPSByYW5kb21TdHJpbmcoKTtcblxuYmVmb3JlQWxsKGFzeW5jICgpID0+IHtcbiAgYXdhaXQgcHJlcGFyZUFwcEZpeHR1cmUoKTtcbn0pO1xuXG5iZWZvcmVFYWNoKGFzeW5jICgpID0+IHtcbiAgYXdhaXQgY2xlYW51cCgpO1xufSk7XG5cbmFmdGVyRWFjaChhc3luYyAoKSA9PiB7XG4gIGF3YWl0IGNsZWFudXAoKTtcbn0pO1xuXG50ZXN0KCdjYW4gYm9vdHN0cmFwIHdpdGhvdXQgZXhlY3V0aW9uJywgYXN5bmMgKCkgPT4ge1xuICBjb25zdCBib290c3RyYXBTdGFja05hbWUgPSBmdWxsU3RhY2tOYW1lKCdib290c3RyYXAtc3RhY2snKTtcblxuICBhd2FpdCBjZGsoWydib290c3RyYXAnLFxuICAgICctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICAnLS1uby1leGVjdXRlJ10pO1xuXG4gIGNvbnN0IHJlc3AgPSBhd2FpdCBjbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBib290c3RyYXBTdGFja05hbWUsXG4gIH0pO1xuXG4gIGV4cGVjdChyZXNwLlN0YWNrcz8uWzBdLlN0YWNrU3RhdHVzKS50b0VxdWFsKCdSRVZJRVdfSU5fUFJPR1JFU1MnKTtcbn0pO1xuXG50ZXN0KCd1cGdyYWRlIGxlZ2FjeSBib290c3RyYXAgc3RhY2sgdG8gbmV3IGJvb3RzdHJhcCBzdGFjayB3aGlsZSBpbiB1c2UnLCBhc3luYyAoKSA9PiB7XG4gIGNvbnN0IGJvb3RzdHJhcFN0YWNrTmFtZSA9IGZ1bGxTdGFja05hbWUoJ2Jvb3RzdHJhcC1zdGFjaycpO1xuXG4gIGNvbnN0IGxlZ2FjeUJvb3RzdHJhcEJ1Y2tldE5hbWUgPSBgYXdzLWNkay1ib290c3RyYXAtaW50ZWctdGVzdC1sZWdhY3ktYmNrdC0ke3JhbmRvbVN0cmluZygpfWA7XG4gIGNvbnN0IG5ld0Jvb3RzdHJhcEJ1Y2tldE5hbWUgPSBgYXdzLWNkay1ib290c3RyYXAtaW50ZWctdGVzdC12Mi1iY2t0LSR7cmFuZG9tU3RyaW5nKCl9YDtcbiAgcmVtZW1iZXJUb0RlbGV0ZUJ1Y2tldChsZWdhY3lCb290c3RyYXBCdWNrZXROYW1lKTsgIC8vIFRoaXMgb25lIHdpbGwgbGVha1xuICByZW1lbWJlclRvRGVsZXRlQnVja2V0KG5ld0Jvb3RzdHJhcEJ1Y2tldE5hbWUpOyAgICAgLy8gVGhpcyBvbmUgc2hvdWxkbid0IGxlYWsgaWYgdGhlIHRlc3Qgc3VjY2VlZHMsIGJ1dCBsZXQncyBiZSBzYWZlIGluIGNhc2UgaXQgZG9lc24ndFxuXG4gIC8vIExlZ2FjeSBib290c3RyYXBcbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJyxcbiAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgJy0tYm9vdHN0cmFwLWJ1Y2tldC1uYW1lJywgbGVnYWN5Qm9vdHN0cmFwQnVja2V0TmFtZV0pO1xuXG4gIC8vIERlcGxveSBzdGFjayB0aGF0IHVzZXMgZmlsZSBhc3NldHNcbiAgYXdhaXQgY2RrRGVwbG95KCdsYW1iZGEnLCB7XG4gICAgb3B0aW9uczogWyctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZV0sXG4gIH0pO1xuXG4gIC8vIFVwZ3JhZGUgYm9vdHN0cmFwIHN0YWNrIHRvIFwibmV3XCIgc3R5bGVcbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJyxcbiAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgJy0tYm9vdHN0cmFwLWJ1Y2tldC1uYW1lJywgbmV3Qm9vdHN0cmFwQnVja2V0TmFtZSxcbiAgICAnLS1xdWFsaWZpZXInLCBRVUFMSUZJRVJdLCB7XG4gICAgbW9kRW52OiB7XG4gICAgICBDREtfTkVXX0JPT1RTVFJBUDogJzEnLFxuICAgIH0sXG4gIH0pO1xuXG4gIC8vIChGb3JjZSkgZGVwbG95IHN0YWNrIGFnYWluXG4gIC8vIC0tZm9yY2UgdG8gYnlwYXNzIHRoZSBjaGVjayB3aGljaCBzYXlzIHRoYXQgdGhlIHRlbXBsYXRlIGhhc24ndCBjaGFuZ2VkLlxuICBhd2FpdCBjZGtEZXBsb3koJ2xhbWJkYScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgICAnLS1mb3JjZScsXG4gICAgXSxcbiAgfSk7XG59KTtcblxudGVzdCgnZGVwbG95IG5ldyBzdHlsZSBzeW50aGVzaXMgdG8gbmV3IHN0eWxlIGJvb3RzdHJhcCcsIGFzeW5jICgpID0+IHtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lID0gZnVsbFN0YWNrTmFtZSgnYm9vdHN0cmFwLXN0YWNrJyk7XG5cbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJyxcbiAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgJy0tcXVhbGlmaWVyJywgUVVBTElGSUVSLFxuICAgICctLWNsb3VkZm9ybWF0aW9uLWV4ZWN1dGlvbi1wb2xpY2llcycsICdhcm46YXdzOmlhbTo6YXdzOnBvbGljeS9BZG1pbmlzdHJhdG9yQWNjZXNzJyxcbiAgXSwge1xuICAgIG1vZEVudjoge1xuICAgICAgQ0RLX05FV19CT09UU1RSQVA6ICcxJyxcbiAgICB9LFxuICB9KTtcblxuICAvLyBEZXBsb3kgc3RhY2sgdGhhdCB1c2VzIGZpbGUgYXNzZXRzXG4gIGF3YWl0IGNka0RlcGxveSgnbGFtYmRhJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXRvb2xraXQtc3RhY2stbmFtZScsIGJvb3RzdHJhcFN0YWNrTmFtZSxcbiAgICAgICctLWNvbnRleHQnLCBgQGF3cy1jZGsvY29yZTpib290c3RyYXBRdWFsaWZpZXI9JHtRVUFMSUZJRVJ9YCxcbiAgICAgICctLWNvbnRleHQnLCAnQGF3cy1jZGsvY29yZTpuZXdTdHlsZVN0YWNrU3ludGhlc2lzPTEnLFxuICAgIF0sXG4gIH0pO1xufSk7XG5cbnRlc3QoJ2RlcGxveSBvbGQgc3R5bGUgc3ludGhlc2lzIHRvIG5ldyBzdHlsZSBib290c3RyYXAnLCBhc3luYyAoKSA9PiB7XG4gIGNvbnN0IGJvb3RzdHJhcFN0YWNrTmFtZSA9IGZ1bGxTdGFja05hbWUoJ2Jvb3RzdHJhcC1zdGFjaycpO1xuXG4gIGF3YWl0IGNkayhbJ2Jvb3RzdHJhcCcsXG4gICAgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgICctLXF1YWxpZmllcicsIFFVQUxJRklFUixcbiAgICAnLS1jbG91ZGZvcm1hdGlvbi1leGVjdXRpb24tcG9saWNpZXMnLCAnYXJuOmF3czppYW06OmF3czpwb2xpY3kvQWRtaW5pc3RyYXRvckFjY2VzcycsXG4gIF0sIHtcbiAgICBtb2RFbnY6IHtcbiAgICAgIENES19ORVdfQk9PVFNUUkFQOiAnMScsXG4gICAgfSxcbiAgfSk7XG5cbiAgLy8gRGVwbG95IHN0YWNrIHRoYXQgdXNlcyBmaWxlIGFzc2V0c1xuICBhd2FpdCBjZGtEZXBsb3koJ2xhbWJkYScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUsXG4gICAgXSxcbiAgfSk7XG59KTtcblxudGVzdCgnZGVwbG95aW5nIG5ldyBzdHlsZSBzeW50aGVzaXMgdG8gb2xkIHN0eWxlIGJvb3RzdHJhcCBmYWlscycsIGFzeW5jICgpID0+IHtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lID0gZnVsbFN0YWNrTmFtZSgnYm9vdHN0cmFwLXN0YWNrJyk7XG5cbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJywgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lXSk7XG5cbiAgLy8gRGVwbG95IHN0YWNrIHRoYXQgdXNlcyBmaWxlIGFzc2V0cywgdGhpcyBmYWlscyBiZWNhdXNlIHRoZSBib290c3RyYXAgc3RhY2tcbiAgLy8gaXMgdmVyc2lvbiBjaGVja2VkLlxuICBhd2FpdCBleHBlY3QoY2RrRGVwbG95KCdsYW1iZGEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lLFxuICAgICAgJy0tY29udGV4dCcsICdAYXdzLWNkay9jb3JlOm5ld1N0eWxlU3RhY2tTeW50aGVzaXM9MScsXG4gICAgXSxcbiAgfSkpLnJlamVjdHMudG9UaHJvdygnZXhpdGVkIHdpdGggZXJyb3InKTtcbn0pO1xuXG50ZXN0KCdjYW4gY3JlYXRlIG11bHRpcGxlIGxlZ2FjeSBib290c3RyYXAgc3RhY2tzJywgYXN5bmMgKCkgPT4ge1xuICBjb25zdCBib290c3RyYXBTdGFja05hbWUxID0gZnVsbFN0YWNrTmFtZSgnYm9vdHN0cmFwLXN0YWNrLTEnKTtcbiAgY29uc3QgYm9vdHN0cmFwU3RhY2tOYW1lMiA9IGZ1bGxTdGFja05hbWUoJ2Jvb3RzdHJhcC1zdGFjay0yJyk7XG5cbiAgLy8gZGVwbG95IHR3byB0b29sa2l0IHN0YWNrcyBpbnRvIHRoZSBzYW1lIGVudmlyb25tZW50IChzZWUgIzE0MTYpXG4gIC8vIG9uZSB3aXRoIHRhZ3NcbiAgYXdhaXQgY2RrKFsnYm9vdHN0cmFwJywgJy12JywgJy0tdG9vbGtpdC1zdGFjay1uYW1lJywgYm9vdHN0cmFwU3RhY2tOYW1lMSwgJy0tdGFncycsICdGb289QmFyJ10pO1xuICBhd2FpdCBjZGsoWydib290c3RyYXAnLCAnLXYnLCAnLS10b29sa2l0LXN0YWNrLW5hbWUnLCBib290c3RyYXBTdGFja05hbWUyXSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBjbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7IFN0YWNrTmFtZTogYm9vdHN0cmFwU3RhY2tOYW1lMSB9KTtcbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlRhZ3MpLnRvRXF1YWwoW1xuICAgIHsgS2V5OiAnRm9vJywgVmFsdWU6ICdCYXInIH0sXG4gIF0pO1xufSk7XG5cbmZ1bmN0aW9uIHJhbmRvbVN0cmluZygpIHtcbiAgLy8gQ3JhenlcbiAgcmV0dXJuIE1hdGgucmFuZG9tKCkudG9TdHJpbmcoMzYpLnJlcGxhY2UoL1teYS16MC05XSsvZywgJycpO1xufVxuIl19 diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.44.0/test.sh b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.44.0/test.sh new file mode 100755 index 000000000..482956df4 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.44.0/test.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail +scriptdir=$(cd $(dirname $0) && pwd) + +echo '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' +echo 'CLI Integration Tests' +echo '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~' + +current_version=$(node -p "require('${scriptdir}/../../../package.json').version") + +# This allows injecting different versions, not just the current one. +# Useful when testing. +export VERSION_UNDER_TEST=${VERSION_UNDER_TEST:-${current_version}} + +cd $scriptdir + +# Install these dependencies that the tests (written in Jest) need. +# Only if we're not running from the repo, because if we are the +# dependencies have already been installed by the containing 'aws-cdk' package's +# package.json. +if ! npx --no-install jest --version; then + echo 'Looks like we need to install jest first. Hold on.' >& 2 + npm install --prefix . jest aws-sdk +fi + +npx jest --runInBand --verbose "$@" \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.61.1/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.61.1/NOTES.md new file mode 100644 index 000000000..390f3f43a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.61.1/NOTES.md @@ -0,0 +1,2 @@ +Introduced a limitation on bootstrapping qualifier length +(max 10 chars) but old tests were using qualifiers of 12 chars. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.61.1/skip-tests.txt b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.61.1/skip-tests.txt new file mode 100644 index 000000000..fcc22f05e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.61.1/skip-tests.txt @@ -0,0 +1,16 @@ +# This file is empty on purpose. Leave it here as documentation +# and an example. +# +# Copy this file to cli-regression-patches/vX.Y.Z/skip-tests.txt +# and edit it there if you want to exclude certain tests from running +# when performing a certain version's regression tests. +# +# Put a test name on a line by itself to skip it. + +upgrade legacy bootstrap stack to new bootstrap stack while in use +deploy new style synthesis to new style bootstrap +deploy new style synthesis to new style bootstrap (with docker image) +deploy old style synthesis to new style bootstrap +can dump the template, modify and use it to deploy a custom bootstrap stack +switch on termination protection, switch is left alone on re-bootstrap +add tags, left alone on re-bootstrap \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.62.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.62.0/NOTES.md new file mode 100644 index 000000000..a6fe21e02 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.62.0/NOTES.md @@ -0,0 +1,2 @@ +Tests now take longer than hour and cause token expiration. +Added creddentials refreshing in in aws-helper but the older tests don't have it. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.62.0/aws-helpers.js b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.62.0/aws-helpers.js new file mode 100644 index 000000000..038f1f028 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.62.0/aws-helpers.js @@ -0,0 +1,245 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.outputFromStack = exports.deleteBucket = exports.deleteImageRepository = exports.emptyBucket = exports.sleep = exports.retry = exports.isBucketMissingError = exports.isStackMissingError = exports.stackStatus = exports.deleteStacks = exports.sts = exports.lambda = exports.iam = exports.sns = exports.ecr = exports.s3 = exports.cloudFormation = exports.testEnv = void 0; +const AWS = require("aws-sdk"); +const cdk_helpers_1 = require("./cdk-helpers"); + +function chainableCredentials(region) { + const profileName = process.env.AWS_PROFILE; + if (process.env.CODEBUILD_BUILD_ARN && profileName) { + // in codebuild we must assume the role that the cdk uses + // otherwise credentials will just be picked up by the normal sdk + // heuristics and expire after an hour. + // can't use '~' since the SDK doesn't seem to expand it...? + const configPath = `${process.env.HOME}/.aws/config`; + const ini = new AWS.IniLoader().loadFrom({ + filename: configPath, + isConfig: true, + }); + const profile = ini[profileName]; + if (!profile) { + throw new Error(`Profile '${profileName}' does not exist in config file (${configPath})`); + } + const arn = profile.role_arn; + const externalId = profile.external_id; + if (!arn) { + throw new Error(`role_arn does not exist in profile ${profileName}`); + } + if (!externalId) { + throw new Error(`external_id does not exist in profile ${externalId}`); + } + return new AWS.ChainableTemporaryCredentials({ + params: { + RoleArn: arn, + ExternalId: externalId, + RoleSessionName: 'integ-tests', + }, + stsConfig: { + region, + }, + masterCredentials: new AWS.ECSCredentials(), + }); + } + return undefined; +} + +exports.testEnv = async () => { + var _a, _b; + const region = (_b = (_a = process.env.AWS_REGION) !== null && _a !== void 0 ? _a : process.env.AWS_DEFAULT_REGION) !== null && _b !== void 0 ? _b : 'us-east-1'; + const sts = new AWS.STS({ + region: region, + credentials: chainableCredentials(region), + maxRetries: 8, + retryDelayOptions: { base: 500 }, + }); + const response = await sts.getCallerIdentity().promise(); + const ret = { + account: response.Account, + region, + }; + exports.testEnv = () => Promise.resolve(ret); + return ret; +}; + +exports.cloudFormation = makeAwsCaller(AWS.CloudFormation); +exports.s3 = makeAwsCaller(AWS.S3); +exports.ecr = makeAwsCaller(AWS.ECR); +exports.sns = makeAwsCaller(AWS.SNS); +exports.iam = makeAwsCaller(AWS.IAM); +exports.lambda = makeAwsCaller(AWS.Lambda); +exports.sts = makeAwsCaller(AWS.STS); +/** + * Perform an AWS call from nothing + * + * Create the correct client, do the call and resole the promise(). + */ +async function awsCall(ctor, call, request) { + const env = await exports.testEnv(); + const cfn = new ctor({ + region: env.region, + credentials: chainableCredentials(env.region), + maxRetries: 6, + retryDelayOptions: { + base: 500, + }, + }); + const response = cfn[call](request); + try { + return await response.promise(); + } + catch (e) { + const newErr = new Error(`${call}(${JSON.stringify(request)}): ${e.message}`); + newErr.code = e.code; + throw newErr; + } +} +/** + * Factory function to invoke 'awsCall' for specific services. + * + * Not strictly necessary but calling this replaces a whole bunch of annoying generics you otherwise have to type: + * + * ```ts + * export function cloudFormation< + * C extends keyof ServiceCalls, + * >(call: C, request: First[C]>): Promise[C]>> { + * return awsCall(AWS.CloudFormation, call, request); + * } + * ``` + */ +function makeAwsCaller(ctor) { + return (call, request) => { + return awsCall(ctor, call, request); + }; +} +async function deleteStacks(...stackNames) { + if (stackNames.length === 0) { + return; + } + for (const stackName of stackNames) { + await exports.cloudFormation('updateTerminationProtection', { + EnableTerminationProtection: false, + StackName: stackName, + }); + await exports.cloudFormation('deleteStack', { + StackName: stackName, + }); + } + await retry(`Deleting ${stackNames}`, retry.forSeconds(600), async () => { + for (const stackName of stackNames) { + const status = await stackStatus(stackName); + if (status !== undefined && status.endsWith('_FAILED')) { + throw retry.abort(new Error(`'${stackName}' is in state '${status}'`)); + } + if (status !== undefined) { + throw new Error(`Delete of '${stackName}' not complete yet`); + } + } + }); +} +exports.deleteStacks = deleteStacks; +async function stackStatus(stackName) { + var _a; + try { + return (_a = (await exports.cloudFormation('describeStacks', { StackName: stackName })).Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus; + } + catch (e) { + if (isStackMissingError(e)) { + return undefined; + } + throw e; + } +} +exports.stackStatus = stackStatus; +function isStackMissingError(e) { + return e.message.indexOf('does not exist') > -1; +} +exports.isStackMissingError = isStackMissingError; +function isBucketMissingError(e) { + return e.message.indexOf('does not exist') > -1; +} +exports.isBucketMissingError = isBucketMissingError; +/** + * Retry an async operation until a deadline is hit. + * + * Use `retry.forSeconds()` to construct a deadline relative to right now. + * + * Exceptions will cause the operation to retry. Use `retry.abort` to annotate an exception + * to stop the retry and end in a failure. + */ +async function retry(operation, deadline, block) { + let i = 0; + cdk_helpers_1.log(`💈 ${operation}`); + while (true) { + try { + i++; + const ret = await block(); + cdk_helpers_1.log(`💈 ${operation}: succeeded after ${i} attempts`); + return ret; + } + catch (e) { + if (e.abort || Date.now() > deadline.getTime()) { + throw new Error(`${operation}: did not succeed after ${i} attempts: ${e}`); + } + cdk_helpers_1.log(`⏳ ${operation} (${e.message})`); + await sleep(5000); + } + } +} +exports.retry = retry; +/** + * Make a deadline for the `retry` function relative to the current time. + */ +retry.forSeconds = (seconds) => { + return new Date(Date.now() + seconds * 1000); +}; +/** + * Annotate an error to stop the retrying + */ +retry.abort = (e) => { + e.abort = true; + return e; +}; +async function sleep(ms) { + return new Promise(ok => setTimeout(ok, ms)); +} +exports.sleep = sleep; +async function emptyBucket(bucketName) { + const objects = await exports.s3('listObjects', { Bucket: bucketName }); + const deletes = (objects.Contents || []).map(obj => obj.Key || '').filter(d => !!d); + if (deletes.length === 0) { + return Promise.resolve(); + } + return exports.s3('deleteObjects', { + Bucket: bucketName, + Delete: { + Objects: deletes.map(d => ({ Key: d })), + Quiet: false, + }, + }); +} +exports.emptyBucket = emptyBucket; +async function deleteImageRepository(repositoryName) { + await exports.ecr('deleteRepository', { repositoryName, force: true }); +} +exports.deleteImageRepository = deleteImageRepository; +async function deleteBucket(bucketName) { + try { + await emptyBucket(bucketName); + await exports.s3('deleteBucket', { + Bucket: bucketName, + }); + } + catch (e) { + if (isBucketMissingError(e)) { + return; + } + throw e; + } +} +exports.deleteBucket = deleteBucket; +function outputFromStack(key, stack) { + var _a, _b; + return (_b = ((_a = stack.Outputs) !== null && _a !== void 0 ? _a : []).find(o => o.OutputKey === key)) === null || _b === void 0 ? void 0 : _b.OutputValue; +} +exports.outputFromStack = outputFromStack; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYXdzLWhlbHBlcnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJhd3MtaGVscGVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSwrQkFBK0I7QUFDL0IsK0NBQW9DO0FBT3pCLFFBQUEsT0FBTyxHQUFHLEtBQUssSUFBa0IsRUFBRTs7SUFDNUMsTUFBTSxRQUFRLEdBQUcsTUFBTSxJQUFJLEdBQUcsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxpQkFBaUIsRUFBRSxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBRW5FLE1BQU0sR0FBRyxHQUFRO1FBQ2YsT0FBTyxFQUFFLFFBQVEsQ0FBQyxPQUFRO1FBQzFCLE1BQU0sY0FBRSxPQUFPLENBQUMsR0FBRyxDQUFDLFVBQVUsbUNBQUksT0FBTyxDQUFDLEdBQUcsQ0FBQyxrQkFBa0IsbUNBQUksV0FBVztLQUNoRixDQUFDO0lBRUYsZUFBTyxHQUFHLEdBQUcsRUFBRSxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDckMsT0FBTyxHQUFHLENBQUM7QUFDYixDQUFDLENBQUM7QUFFVyxRQUFBLGNBQWMsR0FBRyxhQUFhLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxDQUFDO0FBQ25ELFFBQUEsRUFBRSxHQUFHLGFBQWEsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLENBQUM7QUFDM0IsUUFBQSxHQUFHLEdBQUcsYUFBYSxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsQ0FBQztBQUM3QixRQUFBLEdBQUcsR0FBRyxhQUFhLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0FBQzdCLFFBQUEsR0FBRyxHQUFHLGFBQWEsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLENBQUM7QUFDN0IsUUFBQSxNQUFNLEdBQUcsYUFBYSxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsQ0FBQztBQUNuQyxRQUFBLEdBQUcsR0FBRyxhQUFhLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0FB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Q0QsT0FBTyxVQUFFLENBQUMsZUFBZSxFQUFFO1FBQ3pCLE1BQU0sRUFBRSxVQUFVO1FBQ2xCLE1BQU0sRUFBRTtZQUNOLE9BQU8sRUFBRSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxFQUFFLEdBQUcsRUFBRSxDQUFDLEVBQUUsQ0FBQyxDQUFDO1lBQ3ZDLEtBQUssRUFBRSxLQUFLO1NBQ2I7S0FDRixDQUFDLENBQUM7QUFDTCxDQUFDO0FBYkQsa0NBYUM7QUFFTSxLQUFLLFVBQVUscUJBQXFCLENBQUMsY0FBc0I7SUFDaEUsTUFBTSxXQUFHLENBQUMsa0JBQWtCLEVBQUUsRUFBRSxjQUFjLEVBQUUsS0FBSyxFQUFFLElBQUksRUFBRSxDQUFDLENBQUM7QUFDakUsQ0FBQztBQUZELHNEQUVDO0FBRU0sS0FBSyxVQUFVLFlBQVksQ0FBQyxVQUFrQjtJQUNuRCxJQUFJO1FBQ0YsTUFBTSxXQUFXLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDOUIsTUFBTSxVQUFFLENBQUMsY0FBYyxFQUFFO1lBQ3ZCLE1BQU0sRUFBRSxVQUFVO1NBQ25CLENBQUMsQ0FBQztLQUNKO0lBQUMsT0FBTyxDQUFDLEVBQUU7UUFDVixJQUFJLG9CQUFvQixDQUFDLENBQUMsQ0FBQyxFQUFFO1lBQUUsT0FBTztTQUFFO1FBQ3hDLE1BQU0sQ0FBQyxDQUFDO0tBQ1Q7QUFDSCxDQUFDO0FBVkQsb0NBVUM7QUFFRCxTQUFnQixlQUFlLENBQUMsR0FBVyxFQUFFLEtBQStCOztJQUMxRSxhQUFPLE9BQUMsS0FBSyxDQUFDLE9BQU8sbUNBQUksRUFBRSxDQUFDLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLFNBQVMsS0FBSyxHQUFHLENBQUMsMENBQUUsV0FBVyxDQUFDO0FBQzNFLENBQUM7QUFGRCwwQ0FFQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIEFXUyBmcm9tICdhd3Mtc2RrJztcbmltcG9ydCB7IGxvZyB9IGZyb20gJy4vY2RrLWhlbHBlcnMnO1xuXG5pbnRlcmZhY2UgRW52IHtcbiAgYWNjb3VudDogc3RyaW5nO1xuICByZWdpb246IHN0cmluZztcbn1cblxuZXhwb3J0IGxldCB0ZXN0RW52ID0gYXN5bmMgKCk6IFByb21pc2U8RW52PiA9PiB7XG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgbmV3IEFXUy5TVFMoKS5nZXRDYWxsZXJJZGVudGl0eSgpLnByb21pc2UoKTtcblxuICBjb25zdCByZXQ6IEVudiA9IHtcbiAgICBhY2NvdW50OiByZXNwb25zZS5BY2NvdW50ISxcbiAgICByZWdpb246IHByb2Nlc3MuZW52LkFXU19SRUdJT04gPz8gcHJvY2Vzcy5lbnYuQVdTX0RFRkFVTFRfUkVHSU9OID8/ICd1cy1lYXN0LTEnLFxuICB9O1xuXG4gIHRlc3RFbnYgPSAoKSA9PiBQcm9taXNlLnJlc29sdmUocmV0KTtcbiAgcmV0dXJuIHJldDtcbn07XG5cbmV4cG9ydCBjb25zdCBjbG91ZEZvcm1hdGlvbiA9IG1ha2VBd3NDYWxsZXIoQVdTLkNsb3VkRm9ybWF0aW9uKTtcbmV4cG9ydCBjb25zdCBzMyA9IG1ha2VBd3NDYWxsZXIoQVdTLlMzKTtcbmV4cG9ydCBjb25zdCBlY3IgPSBtYWtlQXdzQ2FsbGVyKEFXUy5FQ1IpO1xuZXhwb3J0IGNvbnN0IHNucyA9IG1ha2VBd3NDYWxsZXIoQVdTLlNOUyk7XG5leHBvcnQgY29uc3QgaWFtID0gbWFrZUF3c0NhbGxlcihBV1MuSUFNKTtcbmV4cG9ydCBjb25zdCBsYW1iZGEgPSBtYWtlQXdzQ2FsbGVyKEFXUy5MYW1iZGEpO1xuZXhwb3J0IGNvbnN0IHN0cyA9IG1ha2VBd3NDYWxsZXIoQVdTLlNUUyk7XG5cbi8qKlxuICogUGVyZm9ybSBhbiBBV1MgY2FsbCBmcm9tIG5vdGhpbmdcbiAqXG4gKiBDcmVhdGUgdGhlIGNvcnJlY3QgY2xpZW50LCBkbyB0aGUgY2FsbCBhbmQgcmVzb2xlIHRoZSBwcm9taXNlKCkuXG4gKi9cbmFzeW5jIGZ1bmN0aW9uIGF3c0NhbGw8XG4gIEEgZXh0ZW5kcyBBV1MuU2VydmljZSxcbiAgQiBleHRlbmRzIGtleW9mIFNlcnZpY2VDYWxsczxBPixcbj4oY3RvcjogbmV3IChjb25maWc6IGFueSkgPT4gQSwgY2FsbDogQiwgcmVxdWVzdDogRmlyc3Q8U2VydmljZUNhbGxzPEE+W0JdPik6IFByb21pc2U8U2Vjb25kPFNlcnZpY2VDYWxsczxBPltCXT4+IHtcbiAgY29uc3QgZW52ID0gYXdhaXQgdGVzdEVudigpO1xuXG4gIGNvbnN0IHByb2ZpbGVOYW1lID0gcHJvY2Vzcy5lbnYuQVdTX1BST0ZJTEU7XG4gIGxldCBjcmVkcyA9IHVuZGVmaW5lZDtcbiAgaWYgKHByb2Nlc3MuZW52LkNPREVCVUlMRF9CVUlMRF9BUk4gJiYgcHJvZmlsZU5hbWUpIHtcblxuICAgIC8vIGluIGNvZGVidWlsZCB3ZSBtdXN0IGFzc3VtZSB0aGUgcm9sZSB0aGF0IHRoZSBjZGsgdXNlc1xuICAgIC8vIG90aGVyd2lzZSBjcmVkZW50aWFscyB3aWxsIGp1c3QgYmUgcGlja2VkIHVwIGJ5IHRoZSBub3JtYWwgc2RrXG4gICAgLy8gaGV1cmlzdGljcyBhbmQgZXhwaXJlIGFmdGVyIGFuIGhvdXIuXG5cbiAgICAvLyBjYW4ndCB1c2UgJ34nIHNpbmNlIHRoZSBTREsgZG9lc24ndCBzZWVtIHRvIGV4cGFuZCBpdC4uLj9cbiAgICBjb25zdCBjb25maWdQYXRoID0gYCR7cHJvY2Vzcy5lbnYuSE9NRX0vLmF3cy9jb25maWdgO1xuICAgIGNvbnN0IGluaSA9IG5ldyBBV1MuSW5pTG9hZGVyKCkubG9hZEZyb20oe1xuICAgICAgZmlsZW5hbWU6IGNvbmZpZ1BhdGgsXG4gICAgICBpc0NvbmZpZzogdHJ1ZSxcbiAgICB9KTtcblxuICAgIGNvbnN0IHByb2ZpbGUgPSBpbmlbcHJvZmlsZU5hbWVdO1xuXG4gICAgaWYgKCFwcm9maWxlKSB7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoYFByb2ZpbGUgJyR7cHJvZmlsZU5hbWV9JyBkb2VzIG5vdCBleGlzdCBpbiBjb25maWcgZmlsZSAoJHtjb25maWdQYXRofSlgKTtcbiAgICB9XG5cbiAgICBjb25zdCBhcm4gPSBwcm9maWxlLnJvbGVfYXJuO1xuICAgIGNvbnN0IGV4dGVybmFsSWQgPSBwcm9maWxlLmV4dGVybmFsX2lkO1xuXG4gICAgaWYgKCFhcm4pIHtcbiAgICAgIHRocm93IG5ldyBFcnJvcihgcm9sZV9hcm4gZG9lcyBub3QgZXhpc3QgaW4gcHJvZmlsZSAke3Byb2ZpbGVOYW1lfWApO1xuICAgIH1cblxuICAgIGlmICghZXh0ZXJuYWxJZCkge1xuICAgICAgdGhyb3cgbmV3IEVycm9yKGBleHRlcm5hbF9pZCBkb2VzIG5vdCBleGlzdCBpbiBwcm9maWxlICR7ZXh0ZXJuYWxJZH1gKTtcbiAgICB9XG5cbiAgICBjcmVkcyA9IG5ldyBBV1MuQ2hhaW5hYmxlVGVtcG9yYXJ5Q3JlZGVudGlhbHMoe1xuICAgICAgcGFyYW1zOiB7XG4gICAgICAgIFJvbGVBcm46IGFybixcbiAgICAgICAgRXh0ZXJuYWxJZDogZXh0ZXJuYWxJZCxcbiAgICAgICAgUm9sZVNlc3Npb25OYW1lOiAnaW50ZWctdGVzdHMnLFxuICAgICAgfSxcbiAgICAgIHN0c0NvbmZpZzoge1xuICAgICAgICByZWdpb246IGVudi5yZWdpb24sXG4gICAgICB9LFxuICAgICAgbWFzdGVyQ3JlZGVudGlhbHM6IG5ldyBBV1MuRUNTQ3JlZGVudGlhbHMoKSxcbiAgICB9KTtcblxuICB9XG5cbiAgY29uc3QgY2ZuID0gbmV3IGN0b3Ioe1xuICAgIHJlZ2lvbjogZW52LnJlZ2lvbixcbiAgICBjcmVkZW50aWFsczogY3JlZHMsXG4gICAgbWF4UmV0cmllczogNixcbiAgICByZXRyeURlbGF5T3B0aW9uczoge1xuICAgICAgYmFzZTogNTAwLFxuICAgIH0sXG4gIH0pO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gY2ZuW2NhbGxdKHJlcXVlc3QpO1xuICB0cnkge1xuICAgIHJldHVybiBhd2FpdCByZXNwb25zZS5wcm9taXNlKCk7XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICBjb25zdCBuZXdFcnIgPSBuZXcgRXJyb3IoYCR7Y2FsbH0oJHtKU09OLnN0cmluZ2lmeShyZXF1ZXN0KX0pOiAke2UubWVzc2FnZX1gKTtcbiAgICAobmV3RXJyIGFzIGFueSkuY29kZSA9IGUuY29kZTtcbiAgICB0aHJvdyBuZXdFcnI7XG4gIH1cbn1cblxuLyoqXG4gKiBGYWN0b3J5IGZ1bmN0aW9uIHRvIGludm9rZSAnYXdzQ2FsbCcgZm9yIHNwZWNpZmljIHNlcnZpY2VzLlxuICpcbiAqIE5vdCBzdHJpY3RseSBuZWNlc3NhcnkgYnV0IGNhbGxpbmcgdGhpcyByZXBsYWNlcyBhIHdob2xlIGJ1bmNoIG9mIGFubm95aW5nIGdlbmVyaWNzIHlvdSBvdGhlcndpc2UgaGF2ZSB0byB0eXBlOlxuICpcbiAqIGBgYHRzXG4gKiBleHBvcnQgZnVuY3Rpb24gY2xvdWRGb3JtYXRpb248XG4gKiAgIEMgZXh0ZW5kcyBrZXlvZiBTZXJ2aWNlQ2FsbHM8QVdTLkNsb3VkRm9ybWF0aW9uPixcbiAqID4oY2FsbDogQywgcmVxdWVzdDogRmlyc3Q8U2VydmljZUNhbGxzPEFXUy5DbG91ZEZvcm1hdGlvbj5bQ10+KTogUHJvbWlzZTxTZWNvbmQ8U2VydmljZUNhbGxzPEFXUy5DbG91ZEZvcm1hdGlvbj5bQ10+PiB7XG4gKiAgIHJldHVybiBhd3NDYWxsKEFXUy5DbG91ZEZvcm1hdGlvbiwgY2FsbCwgcmVxdWVzdCk7XG4gKiB9XG4gKiBgYGBcbiAqL1xuZnVuY3Rpb24gbWFrZUF3c0NhbGxlcjxBIGV4dGVuZHMgQVdTLlNlcnZpY2U+KGN0b3I6IG5ldyAoY29uZmlnOiBhbnkpID0+IEEpIHtcbiAgcmV0dXJuIDxCIGV4dGVuZHMga2V5b2YgU2VydmljZUNhbGxzPEE+PihjYWxsOiBCLCByZXF1ZXN0OiBGaXJzdDxTZXJ2aWNlQ2FsbHM8QT5bQl0+KTogUHJvbWlzZTxTZWNvbmQ8U2VydmljZUNhbGxzPEE+W0JdPj4gPT4ge1xuICAgIHJldHVybiBhd3NDYWxsKGN0b3IsIGNhbGwsIHJlcXVlc3QpO1xuICB9O1xufVxuXG50eXBlIFNlcnZpY2VDYWxsczxUPiA9IE5vTmF5TmV2ZXI8U2ltcGxpZmllZFNlcnZpY2U8VD4+O1xuLy8gTWFwIGV2ZXIgbWVtYmVyIGluIHRoZSB0eXBlIHRvIHRoZSBpbXBvcnRhbnQgQVdTIGNhbGwgb3ZlcmxvYWQsIG9yIHRvICduZXZlcidcbnR5cGUgU2ltcGxpZmllZFNlcnZpY2U8VD4gPSB7W2sgaW4ga2V5b2YgVF06IEF3c0NhbGxJTzxUW2tdPn07XG4vLyBSZW1vdmUgYWxsICduZXZlcicgdHlwZXMgZnJvbSBhbiBvYmplY3QgdHlwZVxudHlwZSBOb05heU5ldmVyPFQ+ID0gUGljazxULCB7W2sgaW4ga2V5b2YgVF06IFRba10gZXh0ZW5kcyBuZXZlciA/IG5ldmVyIDogayB9W2tleW9mIFRdPjtcblxuLy8gQmVjYXVzZSBvZiB0aGUgb3ZlcmxvYWRzIGFuIEFXUyBoYW5kbGVyIHR5cGUgbG9va3MgbGlrZSB0aGlzOlxuLy9cbi8vICAge1xuLy8gICAgICAocGFyYW1zOiBJTlBVVFNUUlVDVCwgY2FsbGJhY2s/OiAoKGVycjogQVdTRXJyb3IsIGRhdGE6IHt9KSA9PiB2b2lkKSB8IHVuZGVmaW5lZCk6IFJlcXVlc3Q8T1VUUFVULCAuLi4+O1xuLy8gICAgICAoY2FsbGJhY2s/OiAoKGVycjogQVdTLkFXU0Vycm9yLCBkYXRhOiB7fSkgPT4gdm9pZCkgfCB1bmRlZmluZWQpOiBBV1MuUmVxdWVzdDwuLi4+O1xuLy8gICB9XG4vL1xuLy8gR2V0IHRoZSBmaXJzdCBvdmVybG9hZCBhbmQgZXh0cmFjdCB0aGUgaW5wdXQgYW5kIG91dHB1dCBzdHJ1Y3QgdHlwZXNcbnR5cGUgQXdzQ2FsbElPPFQ+ID1cbiAgVCBleHRlbmRzIHtcbiAgICAoYXJnczogaW5mZXIgSU5QVVQsIGNhbGxiYWNrPzogKChlcnI6IEFXUy5BV1NFcnJvciwgZGF0YTogYW55KSA9PiB2b2lkKSB8IHVuZGVmaW5lZCk6IEFXUy5SZXF1ZXN0PGluZmVyIE9VVFBVVCwgQVdTLkFXU0Vycm9yPjtcbiAgICAoY2FsbGJhY2s/OiAoKGVycjogQVdTLkFXU0Vycm9yLCBkYXRhOiB7fSkgPT4gdm9pZCkgfCB1bmRlZmluZWQpOiBBV1MuUmVxdWVzdDxhbnksIGFueT47XG4gIH0gPyBbSU5QVVQsIE9VVFBVVF0gOiBuZXZlcjtcblxudHlwZSBGaXJzdDxUPiA9IFQgZXh0ZW5kcyBbYW55LCBhbnldID8gVFswXSA6IG5ldmVyO1xudHlwZSBTZWNvbmQ8VD4gPSBUIGV4dGVuZHMgW2FueSwgYW55XSA/IFRbMV0gOiBuZXZlcjtcblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGRlbGV0ZVN0YWNrcyguLi5zdGFja05hbWVzOiBzdHJpbmdbXSkge1xuICBpZiAoc3RhY2tOYW1lcy5sZW5ndGggPT09IDApIHsgcmV0dXJuOyB9XG5cbiAgZm9yIChjb25zdCBzdGFja05hbWUgb2Ygc3RhY2tOYW1lcykge1xuICAgIGF3YWl0IGNsb3VkRm9ybWF0aW9uKCd1cGRhdGVUZXJtaW5hdGlvblByb3RlY3Rpb24nLCB7XG4gICAgICBFbmFibGVUZXJtaW5hdGlvblByb3RlY3Rpb246IGZhbHNlLFxuICAgICAgU3RhY2tOYW1lOiBzdGFja05hbWUsXG4gICAgfSk7XG4gICAgYXdhaXQgY2xvdWRGb3JtYXRpb24oJ2RlbGV0ZVN0YWNrJywge1xuICAgICAgU3RhY2tOYW1lOiBzdGFja05hbWUsXG4gICAgfSk7XG4gIH1cblxuICBhd2FpdCByZXRyeShgRGVsZXRpbmcgJHtzdGFja05hbWVzfWAsIHJldHJ5LmZvclNlY29uZHMoNjAwKSwgYXN5bmMgKCkgPT4ge1xuICAgIGZvciAoY29uc3Qgc3RhY2tOYW1lIG9mIHN0YWNrTmFtZXMpIHtcbiAgICAgIGNvbnN0IHN0YXR1cyA9IGF3YWl0IHN0YWNrU3RhdHVzKHN0YWNrTmFtZSk7XG4gICAgICBpZiAoc3RhdHVzICE9PSB1bmRlZmluZWQgJiYgc3RhdHVzLmVuZHNXaXRoKCdfRkFJTEVEJykpIHtcbiAgICAgICAgdGhyb3cgcmV0cnkuYWJvcnQobmV3IEVycm9yKGAnJHtzdGFja05hbWV9JyBpcyBpbiBzdGF0ZSAnJHtzdGF0dXN9J2ApKTtcbiAgICAgIH1cbiAgICAgIGlmIChzdGF0dXMgIT09IHVuZGVmaW5lZCkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYERlbGV0ZSBvZiAnJHtzdGFja05hbWV9JyBub3QgY29tcGxldGUgeWV0YCk7XG4gICAgICB9XG4gICAgfVxuICB9KTtcbn1cblxuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHN0YWNrU3RhdHVzKHN0YWNrTmFtZTogc3RyaW5nKTogUHJvbWlzZTxzdHJpbmcgfCB1bmRlZmluZWQ+IHtcbiAgdHJ5IHtcbiAgICByZXR1cm4gKGF3YWl0IGNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBzdGFja05hbWUgfSkpLlN0YWNrcz8uWzBdLlN0YWNrU3RhdHVzO1xuICB9IGNhdGNoIChlKSB7XG4gICAgaWYgKGlzU3RhY2tNaXNzaW5nRXJyb3IoZSkpIHsgcmV0dXJuIHVuZGVmaW5lZDsgfVxuICAgIHRocm93IGU7XG4gIH1cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGlzU3RhY2tNaXNzaW5nRXJyb3IoZTogRXJyb3IpIHtcbiAgcmV0dXJuIGUubWVzc2FnZS5pbmRleE9mKCdkb2VzIG5vdCBleGlzdCcpID4gLTE7XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBpc0J1Y2tldE1pc3NpbmdFcnJvcihlOiBFcnJvcikge1xuICByZXR1cm4gZS5tZXNzYWdlLmluZGV4T2YoJ2RvZXMgbm90IGV4aXN0JykgPiAtMTtcbn1cblxuLyoqXG4gKiBSZXRyeSBhbiBhc3luYyBvcGVyYXRpb24gdW50aWwgYSBkZWFkbGluZSBpcyBoaXQuXG4gKlxuICogVXNlIGByZXRyeS5mb3JTZWNvbmRzKClgIHRvIGNvbnN0cnVjdCBhIGRlYWRsaW5lIHJlbGF0aXZlIHRvIHJpZ2h0IG5vdy5cbiAqXG4gKiBFeGNlcHRpb25zIHdpbGwgY2F1c2UgdGhlIG9wZXJhdGlvbiB0byByZXRyeS4gVXNlIGByZXRyeS5hYm9ydGAgdG8gYW5ub3RhdGUgYW4gZXhjZXB0aW9uXG4gKiB0byBzdG9wIHRoZSByZXRyeSBhbmQgZW5kIGluIGEgZmFpbHVyZS5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHJldHJ5PEE+KG9wZXJhdGlvbjogc3RyaW5nLCBkZWFkbGluZTogRGF0ZSwgYmxvY2s6ICgpID0+IFByb21pc2U8QT4pOiBQcm9taXNlPEE+IHtcbiAgbGV0IGkgPSAwO1xuICBsb2coYPCfkoggJHtvcGVyYXRpb259YCk7XG4gIHdoaWxlICh0cnVlKSB7XG4gICAgdHJ5IHtcbiAgICAgIGkrKztcbiAgICAgIGNvbnN0IHJldCA9IGF3YWl0IGJsb2NrKCk7XG4gICAgICBsb2coYPCfkoggJHtvcGVyYXRpb259OiBzdWNjZWVkZWQgYWZ0ZXIgJHtpfSBhdHRlbXB0c2ApO1xuICAgICAgcmV0dXJuIHJldDtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICBpZiAoZS5hYm9ydCB8fCBEYXRlLm5vdygpID4gZGVhZGxpbmUuZ2V0VGltZSggKSkge1xuICAgICAgICB0aHJvdyBuZXcgRXJyb3IoYCR7b3BlcmF0aW9ufTogZGlkIG5vdCBzdWNjZWVkIGFmdGVyICR7aX0gYXR0ZW1wdHM6ICR7ZX1gKTtcbiAgICAgIH1cbiAgICAgIGxvZyhg4o+zICR7b3BlcmF0aW9ufSAoJHtlLm1lc3NhZ2V9KWApO1xuICAgICAgYXdhaXQgc2xlZXAoNTAwMCk7XG4gICAgfVxuICB9XG59XG5cbi8qKlxuICogTWFrZSBhIGRlYWRsaW5lIGZvciB0aGUgYHJldHJ5YCBmdW5jdGlvbiByZWxhdGl2ZSB0byB0aGUgY3VycmVudCB0aW1lLlxuICovXG5yZXRyeS5mb3JTZWNvbmRzID0gKHNlY29uZHM6IG51bWJlcik6IERhdGUgPT4ge1xuICByZXR1cm4gbmV3IERhdGUoRGF0ZS5ub3coKSArIHNlY29uZHMgKiAxMDAwKTtcbn07XG5cbi8qKlxuICogQW5ub3RhdGUgYW4gZXJyb3IgdG8gc3RvcCB0aGUgcmV0cnlpbmdcbiAqL1xucmV0cnkuYWJvcnQgPSAoZTogRXJyb3IpOiBFcnJvciA9PiB7XG4gIChlIGFzIGFueSkuYWJvcnQgPSB0cnVlO1xuICByZXR1cm4gZTtcbn07XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBzbGVlcChtczogbnVtYmVyKSB7XG4gIHJldHVybiBuZXcgUHJvbWlzZShvayA9PiBzZXRUaW1lb3V0KG9rLCBtcykpO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gZW1wdHlCdWNrZXQoYnVja2V0TmFtZTogc3RyaW5nKSB7XG4gIGNvbnN0IG9iamVjdHMgPSBhd2FpdCBzMygnbGlzdE9iamVjdHMnLCB7IEJ1Y2tldDogYnVja2V0TmFtZSB9KTtcbiAgY29uc3QgZGVsZXRlcyA9IChvYmplY3RzLkNvbnRlbnRzIHx8IFtdKS5tYXAob2JqID0+IG9iai5LZXkgfHwgJycpLmZpbHRlcihkID0+ICEhZCk7XG4gIGlmIChkZWxldGVzLmxlbmd0aCA9PT0gMCkge1xuICAgIHJldHVybiBQcm9taXNlLnJlc29sdmUoKTtcbiAgfVxuICByZXR1cm4gczMoJ2RlbGV0ZU9iamVjdHMnLCB7XG4gICAgQnVja2V0OiBidWNrZXROYW1lLFxuICAgIERlbGV0ZToge1xuICAgICAgT2JqZWN0czogZGVsZXRlcy5tYXAoZCA9PiAoeyBLZXk6IGQgfSkpLFxuICAgICAgUXVpZXQ6IGZhbHNlLFxuICAgIH0sXG4gIH0pO1xufVxuXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gZGVsZXRlSW1hZ2VSZXBvc2l0b3J5KHJlcG9zaXRvcnlOYW1lOiBzdHJpbmcpIHtcbiAgYXdhaXQgZWNyKCdkZWxldGVSZXBvc2l0b3J5JywgeyByZXBvc2l0b3J5TmFtZSwgZm9yY2U6IHRydWUgfSk7XG59XG5cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBkZWxldGVCdWNrZXQoYnVja2V0TmFtZTogc3RyaW5nKSB7XG4gIHRyeSB7XG4gICAgYXdhaXQgZW1wdHlCdWNrZXQoYnVja2V0TmFtZSk7XG4gICAgYXdhaXQgczMoJ2RlbGV0ZUJ1Y2tldCcsIHtcbiAgICAgIEJ1Y2tldDogYnVja2V0TmFtZSxcbiAgICB9KTtcbiAgfSBjYXRjaCAoZSkge1xuICAgIGlmIChpc0J1Y2tldE1pc3NpbmdFcnJvcihlKSkgeyByZXR1cm47IH1cbiAgICB0aHJvdyBlO1xuICB9XG59XG5cbmV4cG9ydCBmdW5jdGlvbiBvdXRwdXRGcm9tU3RhY2soa2V5OiBzdHJpbmcsIHN0YWNrOiBBV1MuQ2xvdWRGb3JtYXRpb24uU3RhY2spOiBzdHJpbmcgfCB1bmRlZmluZWQge1xuICByZXR1cm4gKHN0YWNrLk91dHB1dHMgPz8gW10pLmZpbmQobyA9PiBvLk91dHB1dEtleSA9PT0ga2V5KT8uT3V0cHV0VmFsdWU7XG59XG4iXX0= \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.63.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.63.0/NOTES.md new file mode 100644 index 000000000..1a2609d91 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.63.0/NOTES.md @@ -0,0 +1 @@ +We made --cloudformation-execution-policies required, old tests don't pass it yet \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.63.0/skip-tests.txt b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.63.0/skip-tests.txt new file mode 100644 index 000000000..ee9ae2180 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.63.0/skip-tests.txt @@ -0,0 +1,7 @@ +# We made --cloudformation-execution-policies required, old tests don't pass it yet + +upgrade legacy bootstrap stack to new bootstrap stack while in use +deploy new style synthesis to new style bootstrap +deploy new style synthesis to new style bootstrap (with docker image) +switch on termination protection, switch is left alone on re-bootstrap +add tags, left alone on re-bootstrap diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.0/NOTES.md new file mode 100644 index 000000000..1cb31072a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.0/NOTES.md @@ -0,0 +1,3 @@ +Added a `-v` switch to the cdk executions that also needs to be +applied to the regression tests so we have a better chance +of catching sporadically failing tests in the act. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.0/cdk-helpers.js b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.0/cdk-helpers.js new file mode 100644 index 000000000..d30e4db96 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.0/cdk-helpers.js @@ -0,0 +1,325 @@ +"use strict"; +var _a, _b; +Object.defineProperty(exports, "__esModule", { value: true }); +const child_process = require("child_process"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const aws_helpers_1 = require("./aws-helpers"); +const resource_pool_1 = require("./resource-pool"); +const REGIONS = process.env.AWS_REGIONS +? process.env.AWS_REGIONS.split(',') +: [(_b = (_a = process.env.AWS_REGION) !== null && _a !== void 0 ? _a : process.env.AWS_DEFAULT_REGION) !== null && _b !== void 0 ? _b : 'us-east-1']; +process.stdout.write(`Using regions: ${REGIONS}\n`); +const REGION_POOL = new resource_pool_1.ResourcePool(REGIONS); +/** + * Higher order function to execute a block with an AWS client setup + * + * Allocate the next region from the REGION pool and dispose it afterwards. + */ +function withAws(block) { +return (context) => REGION_POOL.using(async (region) => { + const aws = await aws_helpers_1.AwsClients.forRegion(region, context.output); + await sanityCheck(aws); + return block({ ...context, aws }); +}); +} +exports.withAws = withAws; +/** + * Higher order function to execute a block with a CDK app fixture + * + * Requires an AWS client to be passed in. + * + * For backwards compatibility with existing tests (so we don't have to change + * too much) the inner block is expected to take a `TestFixture` object. + */ +function withCdkApp(block) { +return async (context) => { + const randy = randomString(); + const stackNamePrefix = `cdktest-${randy}`; + const integTestDir = path.join(os.tmpdir(), `cdk-integ-${randy}`); + context.output.write(` Stack prefix: ${stackNamePrefix}\n`); + context.output.write(` Test directory: ${integTestDir}\n`); + context.output.write(` Region: ${context.aws.region}\n`); + await cloneDirectory(path.join(__dirname, 'app'), integTestDir, context.output); + const fixture = new TestFixture(integTestDir, stackNamePrefix, context.output, context.aws); + let success = true; + try { + await fixture.shell(['npm', 'install', + '@aws-cdk/core', + '@aws-cdk/aws-sns', + '@aws-cdk/aws-iam', + '@aws-cdk/aws-lambda', + '@aws-cdk/aws-ssm', + '@aws-cdk/aws-ecr-assets', + '@aws-cdk/aws-cloudformation', + '@aws-cdk/aws-ec2']); + await ensureBootstrapped(fixture); + await block(fixture); + } + catch (e) { + success = false; + throw e; + } + finally { + await fixture.dispose(success); + } +}; +} +exports.withCdkApp = withCdkApp; +/** + * Default test fixture for most (all?) integ tests + * + * It's a composition of withAws/withCdkApp, expecting the test block to take a `TestFixture` + * object. + * + * We could have put `withAws(withCdkApp(fixture => { /... actual test here.../ }))` in every + * test declaration but centralizing it is going to make it convenient to modify in the future. + */ +function withDefaultFixture(block) { +return withAws(withCdkApp(block)); +// ^~~~~~ this is disappointing TypeScript! Feels like you should have been able to derive this. +} +exports.withDefaultFixture = withDefaultFixture; +/** + * Prepare a target dir byreplicating a source directory + */ +async function cloneDirectory(source, target, output) { +await shell(['rm', '-rf', target], { output }); +await shell(['mkdir', '-p', target], { output }); +await shell(['cp', '-R', source + '/*', target], { output }); +} +exports.cloneDirectory = cloneDirectory; +class TestFixture { +constructor(integTestDir, stackNamePrefix, output, aws) { + this.integTestDir = integTestDir; + this.stackNamePrefix = stackNamePrefix; + this.output = output; + this.aws = aws; + this.qualifier = randomString().slice(0, 10); + this.bucketsToDelete = new Array(); +} +log(s) { + this.output.write(`${s}\n`); +} +async shell(command, options = {}) { + return shell(command, { + output: this.output, + cwd: this.integTestDir, + ...options, + }); +} +async cdkDeploy(stackNames, options = {}) { + var _a, _b; + stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + const neverRequireApproval = (_a = options.neverRequireApproval) !== null && _a !== void 0 ? _a : true; + return this.cdk(['deploy', + ...(neverRequireApproval ? ['--require-approval=never'] : []), + ...((_b = options.options) !== null && _b !== void 0 ? _b : []), + ...this.fullStackName(stackNames)], options); +} +async cdkDestroy(stackNames, options = {}) { + var _a; + stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + return this.cdk(['destroy', + '-f', + ...((_a = options.options) !== null && _a !== void 0 ? _a : []), + ...this.fullStackName(stackNames)], options); +} +async cdk(args, options = {}) { + var _a; + const verbose = (_a = options.verbose) !== null && _a !== void 0 ? _a : true; + return this.shell(['cdk', ...(verbose ? ['-v'] : []), ...args], { + ...options, + modEnv: { + AWS_REGION: this.aws.region, + AWS_DEFAULT_REGION: this.aws.region, + STACK_NAME_PREFIX: this.stackNamePrefix, + ...options.modEnv, + }, + }); +} +fullStackName(stackNames) { + if (typeof stackNames === 'string') { + return `${this.stackNamePrefix}-${stackNames}`; + } + else { + return stackNames.map(s => `${this.stackNamePrefix}-${s}`); + } +} +/** + * Append this to the list of buckets to potentially delete + * + * At the end of a test, we clean up buckets that may not have gotten destroyed + * (for whatever reason). + */ +rememberToDeleteBucket(bucketName) { + this.bucketsToDelete.push(bucketName); +} +/** + * Cleanup leftover stacks and buckets + */ +async dispose(success) { + const stacksToDelete = await this.deleteableStacks(this.stackNamePrefix); + // Bootstrap stacks have buckets that need to be cleaned + const bucketNames = stacksToDelete.map(stack => aws_helpers_1.outputFromStack('BucketName', stack)).filter(defined); + await Promise.all(bucketNames.map(b => this.aws.emptyBucket(b))); + // Bootstrap stacks have ECR repositories with images which should be deleted + const imageRepositoryNames = stacksToDelete.map(stack => aws_helpers_1.outputFromStack('ImageRepositoryName', stack)).filter(defined); + await Promise.all(imageRepositoryNames.map(r => this.aws.deleteImageRepository(r))); + await this.aws.deleteStacks(...stacksToDelete.map(s => s.StackName)); + // We might have leaked some buckets by upgrading the bootstrap stack. Be + // sure to clean everything. + for (const bucket of this.bucketsToDelete) { + await this.aws.deleteBucket(bucket); + } + // If the tests completed successfully, happily delete the fixture + // (otherwise leave it for humans to inspect) + if (success) { + rimraf(this.integTestDir); + } +} +/** + * Return the stacks starting with our testing prefix that should be deleted + */ +async deleteableStacks(prefix) { + var _a; + const statusFilter = [ + 'CREATE_IN_PROGRESS', 'CREATE_FAILED', 'CREATE_COMPLETE', + 'ROLLBACK_IN_PROGRESS', 'ROLLBACK_FAILED', 'ROLLBACK_COMPLETE', + 'DELETE_FAILED', + 'UPDATE_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_COMPLETE', 'UPDATE_ROLLBACK_IN_PROGRESS', + 'UPDATE_ROLLBACK_FAILED', + 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_ROLLBACK_COMPLETE', 'REVIEW_IN_PROGRESS', + 'IMPORT_IN_PROGRESS', 'IMPORT_COMPLETE', + 'IMPORT_ROLLBACK_IN_PROGRESS', 'IMPORT_ROLLBACK_FAILED', + 'IMPORT_ROLLBACK_COMPLETE', + ]; + const response = await this.aws.cloudFormation('describeStacks', {}); + return ((_a = response.Stacks) !== null && _a !== void 0 ? _a : []) + .filter(s => s.StackName.startsWith(prefix)) + .filter(s => statusFilter.includes(s.StackStatus)) + .filter(s => s.RootId === undefined); // Only delete parent stacks. Nested stacks are deleted in the process +} +} +exports.TestFixture = TestFixture; +/** + * Perform a one-time quick sanity check that the AWS clients has properly configured credentials + * + * If we don't do this, calls are going to fail and they'll be retried and everything will take + * forever before the user notices a simple misconfiguration. + * + * We can't check for the presence of environment variables since credentials could come from + * anywhere, so do simple account retrieval. + * + * Only do it once per process. + */ +async function sanityCheck(aws) { +if (sanityChecked === undefined) { + try { + await aws.account(); + sanityChecked = true; + } + catch (e) { + sanityChecked = false; + throw new Error(`AWS credentials probably not configured, got error: ${e.message}`); + } +} +if (!sanityChecked) { + throw new Error('AWS credentials probably not configured, see previous error'); +} +} +let sanityChecked; +/** + * Make sure that the given environment is bootstrapped + * + * Since we go striping across regions, it's going to suck doing this + * by hand so let's just mass-automate it. + */ +async function ensureBootstrapped(fixture) { +// Old-style bootstrap stack with default name +if (await fixture.aws.stackStatus('CDKToolkit') === undefined) { + await fixture.cdk(['bootstrap', `aws://${await fixture.aws.account()}/${fixture.aws.region}`]); +} +} +/** + * A shell command that does what you want + * + * Is platform-aware, handles errors nicely. + */ +async function shell(command, options = {}) { +var _a, _b; +if (options.modEnv && options.env) { + throw new Error('Use either env or modEnv but not both'); +} +(_a = options.output) === null || _a === void 0 ? void 0 : _a.write(`💻 ${command.join(' ')}\n`); +const env = (_b = options.env) !== null && _b !== void 0 ? _b : (options.modEnv ? { ...process.env, ...options.modEnv } : undefined); +const child = child_process.spawn(command[0], command.slice(1), { + ...options, + env, + // Need this for Windows where we want .cmd and .bat to be found as well. + shell: true, + stdio: ['ignore', 'pipe', 'pipe'], +}); +return new Promise((resolve, reject) => { + const stdout = new Array(); + const stderr = new Array(); + child.stdout.on('data', chunk => { + var _a; + (_a = options.output) === null || _a === void 0 ? void 0 : _a.write(chunk); + stdout.push(chunk); + }); + child.stderr.on('data', chunk => { + var _a, _b; + (_a = options.output) === null || _a === void 0 ? void 0 : _a.write(chunk); + if ((_b = options.captureStderr) !== null && _b !== void 0 ? _b : true) { + stderr.push(chunk); + } + }); + child.once('error', reject); + child.once('close', code => { + if (code === 0 || options.allowErrExit) { + resolve((Buffer.concat(stdout).toString('utf-8') + Buffer.concat(stderr).toString('utf-8')).trim()); + } + else { + reject(new Error(`'${command.join(' ')}' exited with error code ${code}`)); + } + }); +}); +} +exports.shell = shell; +function defined(x) { +return x !== undefined; +} +/** + * rm -rf reimplementation, don't want to depend on an NPM package for this + */ +function rimraf(fsPath) { +try { + const isDir = fs.lstatSync(fsPath).isDirectory(); + if (isDir) { + for (const file of fs.readdirSync(fsPath)) { + rimraf(path.join(fsPath, file)); + } + fs.rmdirSync(fsPath); + } + else { + fs.unlinkSync(fsPath); + } +} +catch (e) { + // We will survive ENOENT + if (e.code !== 'ENOENT') { + throw e; + } +} +} +exports.rimraf = rimraf; +function randomString() { +// Crazy +return Math.random().toString(36).replace(/[^a-z0-9]+/g, ''); +} +exports.randomString = randomString; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2RrLWhlbHBlcnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjZGstaGVscGVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSwrQ0FBK0M7QUFDL0MseUJBQXlCO0FBQ3pCLHlCQUF5QjtBQUN6Qiw2QkFBNkI7QUFDN0IsK0NBQTREO0FBQzVELG1EQUErQztBQUcvQyxNQUFNLE9BQU8sR0FBRyxPQUFPLENBQUMsR0FBRyxDQUFDLFdBQVc7SUFDckMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsV0FBVyxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUM7SUFDcEMsQ0FBQyxDQUFDLGFBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxVQUFVLG1DQUFJLE9BQU8sQ0FBQyxHQUFHLENBQUMsa0JBQWtCLG1DQUFJLFdBQVcsQ0FBQyxDQUFDO0FBRTlFLE9BQU8sQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLGtCQUFrQixPQUFPLElBQUksQ0FBQyxDQUFDO0FBRXBELE1BQU0sV0FBVyxHQUFHLElBQUksNEJBQVksQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUs5Qzs7OztHQUlHO0FBQ0gsU0FBZ0IsT0FBTyxDQUF3QixLQUFpRDtJQUM5RixPQUFPLENBQUMsT0FBVSxFQUFFLEVBQUUsQ0FBQyxXQUFXLENBQUMsS0FBSyxDQUFDLEtBQUssRUFBRSxNQUFNLEVBQUUsRUFBRTtRQUN4RCxNQUFNLEdBQUcsR0FBRyxNQUFNLHdCQUFVLENBQUMsU0FBUyxDQUFDLE1BQU0sRUFBRSxPQUFPLENBQUMsTUFBTSxDQUFDLENBQUM7UUFDL0QsTUFBTSxXQUFXLENBQUMsR0FBRyxDQUFDLENBQUM7UUFFdkIsT0FBTyxLQUFLLENBQUMsRUFBRSxHQUFHLE9BQU8sRUFBRSxHQUFHLEVBQUUsQ0FBQyxDQUFDO0lBQ3BDLENBQUMsQ0FBQyxDQUFDO0FBQ0wsQ0FBQztBQVBELDBCQU9DO0FBRUQ7Ozs7Ozs7R0FPRztBQUNILFNBQWdCLFVBQVUsQ0FBcUMsS0FBOEM7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Q0FBQyxDQUFDO1NBQ3RCO2FBQU07WUFDTCxFQUFFLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1NBQ3ZCO0tBQ0Y7SUFBQyxPQUFPLENBQUMsRUFBRTtRQUNWLHlCQUF5QjtRQUN6QixJQUFJLENBQUMsQ0FBQyxJQUFJLEtBQUssUUFBUSxFQUFFO1lBQUUsTUFBTSxDQUFDLENBQUM7U0FBRTtLQUN0QztBQUNILENBQUM7QUFoQkQsd0JBZ0JDO0FBRUQsU0FBZ0IsWUFBWTtJQUMxQixRQUFRO0lBQ1IsT0FBTyxJQUFJLENBQUMsTUFBTSxFQUFFLENBQUMsUUFBUSxDQUFDLEVBQUUsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxhQUFhLEVBQUUsRUFBRSxDQUFDLENBQUM7QUFDL0QsQ0FBQztBQUhELG9DQUdDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgY2hpbGRfcHJvY2VzcyBmcm9tICdjaGlsZF9wcm9jZXNzJztcbmltcG9ydCAqIGFzIGZzIGZyb20gJ2ZzJztcbmltcG9ydCAqIGFzIG9zIGZyb20gJ29zJztcbmltcG9ydCAqIGFzIHBhdGggZnJvbSAncGF0aCc7XG5pbXBvcnQgeyBvdXRwdXRGcm9tU3RhY2ssIEF3c0NsaWVudHMgfSBmcm9tICcuL2F3cy1oZWxwZXJzJztcbmltcG9ydCB7IFJlc291cmNlUG9vbCB9IGZyb20gJy4vcmVzb3VyY2UtcG9vbCc7XG5pbXBvcnQgeyBUZXN0Q29udGV4dCB9IGZyb20gJy4vdGVzdC1oZWxwZXJzJztcblxuY29uc3QgUkVHSU9OUyA9IHByb2Nlc3MuZW52LkFXU19SRUdJT05TXG4gID8gcHJvY2Vzcy5lbnYuQVdTX1JFR0lPTlMuc3BsaXQoJywnKVxuICA6IFtwcm9jZXNzLmVudi5BV1NfUkVHSU9OID8/IHByb2Nlc3MuZW52LkFXU19ERUZBVUxUX1JFR0lPTiA/PyAndXMtZWFzdC0xJ107XG5cbnByb2Nlc3Muc3Rkb3V0LndyaXRlKGBVc2luZyByZWdpb25zOiAke1JFR0lPTlN9XFxuYCk7XG5cbmNvbnN0IFJFR0lPTl9QT09MID0gbmV3IFJlc291cmNlUG9vbChSRUdJT05TKTtcblxuXG5leHBvcnQgdHlwZSBBd3NDb250ZXh0ID0geyByZWFkb25seSBhd3M6IEF3c0NsaWVudHMgfTtcblxuLyoqXG4gKiBIaWdoZXIgb3JkZXIgZnVuY3Rpb24gdG8gZXhlY3V0ZSBhIGJsb2NrIHdpdGggYW4gQVdTIGNsaWVudCBzZXR1cFxuICpcbiAqIEFsbG9jYXRlIHRoZSBuZXh0IHJlZ2lvbiBmcm9tIHRoZSBSRUdJT04gcG9vbCBhbmQgZGlzcG9zZSBpdCBhZnRlcndhcmRzLlxuICovXG5leHBvcnQgZnVuY3Rpb24gd2l0aEF3czxBIGV4dGVuZHMgVGVzdENvbnRleHQ+KGJsb2NrOiAoY29udGV4dDogQSAmIEF3c0NvbnRleHQpID0+IFByb21pc2U8dm9pZD4pIHtcbiAgcmV0dXJuIChjb250ZXh0OiBBKSA9PiBSRUdJT05fUE9PTC51c2luZyhhc3luYyAocmVnaW9uKSA9PiB7XG4gICAgY29uc3QgYXdzID0gYXdhaXQgQXdzQ2xpZW50cy5mb3JSZWdpb24ocmVnaW9uLCBjb250ZXh0Lm91dHB1dCk7XG4gICAgYXdhaXQgc2FuaXR5Q2hlY2soYXdzKTtcblxuICAgIHJldHVybiBibG9jayh7IC4uLmNvbnRleHQsIGF3cyB9KTtcbiAgfSk7XG59XG5cbi8qKlxuICogSGlnaGVyIG9yZGVyIGZ1bmN0aW9uIHRvIGV4ZWN1dGUgYSBibG9jayB3aXRoIGEgQ0RLIGFwcCBmaXh0dXJlXG4gKlxuICogUmVxdWlyZXMgYW4gQVdTIGNsaWVudCB0byBiZSBwYXNzZWQgaW4uXG4gKlxuICogRm9yIGJhY2t3YXJkcyBjb21wYXRpYmlsaXR5IHdpdGggZXhpc3RpbmcgdGVzdHMgKHNvIHdlIGRvbid0IGhhdmUgdG8gY2hhbmdlXG4gKiB0b28gbXVjaCkgdGhlIGlubmVyIGJsb2NrIGlzIGV4cGVjdGVkIHRvIHRha2UgYSBgVGVzdEZpeHR1cmVgIG9iamVjdC5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHdpdGhDZGtBcHA8QSBleHRlbmRzIFRlc3RDb250ZXh0ICYgQXdzQ29udGV4dD4oYmxvY2s6IChjb250ZXh0OiBUZXN0Rml4dHVyZSkgPT4gUHJvbWlzZTx2b2lkPikge1xuICByZXR1cm4gYXN5bmMgKGNvbnRleHQ6IEEpID0+IHtcbiAgICBjb25zdCByYW5keSA9IHJhbmRvbVN0cmluZygpO1xuICAgIGNvbnN0IHN0YWNrTmFtZVByZWZpeCA9IGBjZGt0ZXN0LSR7cmFuZHl9YDtcbiAgICBjb25zdCBpbnRlZ1Rlc3REaXIgPSBwYXRoLmpvaW4ob3MudG1wZGlyKCksIGBjZGstaW50ZWctJHtyYW5keX1gKTtcblxuICAgIGNvbnRleHQub3V0cHV0LndyaXRlKGAgU3RhY2sgcHJlZml4OiAgICR7c3RhY2tOYW1lUHJlZml4fVxcbmApO1xuICAgIGNvbnRleHQub3V0cHV0LndyaXRlKGAgVGVzdCBkaXJlY3Rvcnk6ICR7aW50ZWdUZXN0RGlyfVxcbmApO1xuICAgIGNvbnRleHQub3V0cHV0LndyaXRlKGAgUmVnaW9uOiAgICAgICAgICR7Y29udGV4dC5hd3MucmVnaW9ufVxcbmApO1xuXG4gICAgYXdhaXQgY2xvbmVEaXJlY3RvcnkocGF0aC5qb2luKF9fZGlybmFtZSwgJ2FwcCcpLCBpbnRlZ1Rlc3REaXIsIGNvbnRleHQub3V0cHV0KTtcbiAgICBjb25zdCBmaXh0dXJlID0gbmV3IFRlc3RGaXh0dXJlKFxuICAgICAgaW50ZWdUZXN0RGlyLFxuICAgICAgc3RhY2tOYW1lUHJlZml4LFxuICAgICAgY29udGV4dC5vdXRwdXQsXG4gICAgICBjb250ZXh0LmF3cyk7XG5cbiAgICBsZXQgc3VjY2VzcyA9IHRydWU7XG4gICAgdHJ5IHtcbiAgICAgIGF3YWl0IGZpeHR1cmUuc2hlbGwoWyducG0nLCAnaW5zdGFsbCcsXG4gICAgICAgICdAYXdzLWNkay9jb3JlJyxcbiAgICAgICAgJ0Bhd3MtY2RrL2F3cy1zbnMnLFxuICAgICAgICAnQGF3cy1jZGsvYXdzLWlhbScsXG4gICAgICAgICdAYXdzLWNkay9hd3MtbGFtYmRhJyxcbiAgICAgICAgJ0Bhd3MtY2RrL2F3cy1zc20nLFxuICAgICAgICAnQGF3cy1jZGsvYXdzLWVjci1hc3NldHMnLFxuICAgICAgICAnQGF3cy1jZGsvYXdzLWNsb3VkZm9ybWF0aW9uJyxcbiAgICAgICAgJ0Bhd3MtY2RrL2F3cy1lYzInXSk7XG5cbiAgICAgIGF3YWl0IGVuc3VyZUJvb3RzdHJhcHBlZChmaXh0dXJlKTtcblxuICAgICAgYXdhaXQgYmxvY2soZml4dHVyZSk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgc3VjY2VzcyA9IGZhbHNlO1xuICAgICAgdGhyb3cgZTtcbiAgICB9IGZpbmFsbHkge1xuICAgICAgYXdhaXQgZml4dHVyZS5kaXNwb3NlKHN1Y2Nlc3MpO1xuICAgIH1cbiAgfTtcbn1cblxuLyoqXG4gKiBEZWZhdWx0IHRlc3QgZml4dHVyZSBmb3IgbW9zdCAoYWxsPykgaW50ZWcgdGVzdHNcbiAqXG4gKiBJdCdzIGEgY29tcG9zaXRpb24gb2Ygd2l0aEF3cy93aXRoQ2RrQXBwLCBleHBlY3RpbmcgdGhlIHRlc3QgYmxvY2sgdG8gdGFrZSBhIGBUZXN0Rml4dHVyZWBcbiAqIG9iamVjdC5cbiAqXG4gKiBXZSBjb3VsZCBoYXZlIHB1dCBgd2l0aEF3cyh3aXRoQ2RrQXBwKGZpeHR1cmUgPT4geyAvLi4uIGFjdHVhbCB0ZXN0IGhlcmUuLi4vIH0pKWAgaW4gZXZlcnlcbiAqIHRlc3QgZGVjbGFyYXRpb24gYnV0IGNlbnRyYWxpemluZyBpdCBpcyBnb2luZyB0byBtYWtlIGl0IGNvbnZlbmllbnQgdG8gbW9kaWZ5IGluIHRoZSBmdXR1cmUuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB3aXRoRGVmYXVsdEZpeHR1cmUoYmxvY2s6IChjb250ZXh0OiBUZXN0Rml4dHVyZSkgPT4gUHJvbWlzZTx2b2lkPikge1xuICByZXR1cm4gd2l0aEF3czxUZXN0Q29udGV4dD4od2l0aENka0FwcChibG9jaykpO1xuICAvLyAgICAgICAgICAgICAgXn5+fn5+IHRoaXMgaXMgZGlzYXBwb2ludGluZyBUeXBlU2NyaXB0ISBGZWVscyBsaWtlIHlvdSBzaG91bGQgaGF2ZSBiZWVuIGFibGUgdG8gZGVyaXZlIHRoaXMuXG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgU2hlbGxPcHRpb25zIGV4dGVuZHMgY2hpbGRfcHJvY2Vzcy5TcGF3bk9wdGlvbnMge1xuICAvKipcbiAgICogUHJvcGVydGllcyB0byBhZGQgdG8gJ2VudidcbiAgICovXG4gIG1vZEVudj86IFJlY29yZDxzdHJpbmcsIHN0cmluZz47XG5cbiAgLyoqXG4gICAqIERvbid0IGZhaWwgd2hlbiBleGl0aW5nIHdpdGggYW4gZXJyb3JcbiAgICpcbiAgICogQGRlZmF1bHQgZmFsc2VcbiAgICovXG4gIGFsbG93RXJyRXhpdD86IGJvb2xlYW47XG5cbiAgLyoqXG4gICAqIFdoZXRoZXIgdG8gY2FwdHVyZSBzdGRlcnJcbiAgICpcbiAgICogQGRlZmF1bHQgdHJ1ZVxuICAgKi9cbiAgY2FwdHVyZVN0ZGVycj86IGJvb2xlYW47XG5cbiAgLyoqXG4gICAqIFBhc3Mgb3V0cHV0IGhlcmVcbiAgICovXG4gIG91dHB1dD86IE5vZGVKUy5Xcml0YWJsZVN0cmVhbTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBDZGtDbGlPcHRpb25zIGV4dGVuZHMgU2hlbGxPcHRpb25zIHtcbiAgb3B0aW9ucz86IHN0cmluZ1tdO1xuICBuZXZlclJlcXVpcmVBcHByb3ZhbD86IGJvb2xlYW47XG4gIHZlcmJvc2U/OiBib29sZWFuO1xufVxuXG4vKipcbiAqIFByZXBhcmUgYSB0YXJnZXQgZGlyIGJ5cmVwbGljYXRpbmcgYSBzb3VyY2UgZGlyZWN0b3J5XG4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBjbG9uZURpcmVjdG9yeShzb3VyY2U6IHN0cmluZywgdGFyZ2V0OiBzdHJpbmcsIG91dHB1dD86IE5vZGVKUy5Xcml0YWJsZVN0cmVhbSkge1xuICBhd2FpdCBzaGVsbChbJ3JtJywgJy1yZicsIHRhcmdldF0sIHsgb3V0cHV0IH0pO1xuICBhd2FpdCBzaGVsbChbJ21rZGlyJywgJy1wJywgdGFyZ2V0XSwgeyBvdXRwdXQgfSk7XG4gIGF3YWl0IHNoZWxsKFsnY3AnLCAnLVInLCBzb3VyY2UgKyAnLyonLCB0YXJnZXRdLCB7IG91dHB1dCB9KTtcbn1cblxuZXhwb3J0IGNsYXNzIFRlc3RGaXh0dXJlIHtcbiAgcHVibGljIHJlYWRvbmx5IHF1YWxpZmllciA9IHJhbmRvbVN0cmluZygpLnN1YnN0cigwLCAxMCk7XG4gIHByaXZhdGUgcmVhZG9ubHkgYnVja2V0c1RvRGVsZXRlID0gbmV3IEFycmF5PHN0cmluZz4oKTtcblxuICBjb25zdHJ1Y3RvcihcbiAgICBwdWJsaWMgcmVhZG9ubHkgaW50ZWdUZXN0RGlyOiBzdHJpbmcsXG4gICAgcHVibGljIHJlYWRvbmx5IHN0YWNrTmFtZVByZWZpeDogc3RyaW5nLFxuICAgIHB1YmxpYyByZWFkb25seSBvdXRwdXQ6IE5vZGVKUy5Xcml0YWJsZVN0cmVhbSxcbiAgICBwdWJsaWMgcmVhZG9ubHkgYXdzOiBBd3NDbGllbnRzKSB7XG4gIH1cblxuICBwdWJsaWMgbG9nKHM6IHN0cmluZykge1xuICAgIHRoaXMub3V0cHV0LndyaXRlKGAke3N9XFxuYCk7XG4gIH1cblxuICBwdWJsaWMgYXN5bmMgc2hlbGwoY29tbWFuZDogc3RyaW5nW10sIG9wdGlvbnM6IE9taXQ8U2hlbGxPcHRpb25zLCAnY3dkJ3wnb3V0cHV0Jz4gPSB7fSk6IFByb21pc2U8c3RyaW5nPiB7XG4gICAgcmV0dXJuIHNoZWxsKGNvbW1hbmQsIHtcbiAgICAgIG91dHB1dDogdGhpcy5vdXRwdXQsXG4gICAgICBjd2Q6IHRoaXMuaW50ZWdUZXN0RGlyLFxuICAgICAgLi4ub3B0aW9ucyxcbiAgICB9KTtcbiAgfVxuXG4gIHB1YmxpYyBhc3luYyBjZGtEZXBsb3koc3RhY2tOYW1lczogc3RyaW5nIHwgc3RyaW5nW10sIG9wdGlvbnM6IENka0NsaU9wdGlvbnMgPSB7fSkge1xuICAgIHN0YWNrTmFtZXMgPSB0eXBlb2Ygc3RhY2tOYW1lcyA9PT0gJ3N0cmluZycgPyBbc3RhY2tOYW1lc10gOiBzdGFja05hbWVzO1xuXG4gICAgY29uc3QgbmV2ZXJSZXF1aXJlQXBwcm92YWwgPSBvcHRpb25zLm5ldmVyUmVxdWlyZUFwcHJvdmFsID8/IHRydWU7XG5cbiAgICByZXR1cm4gdGhpcy5jZGsoWydkZXBsb3knLFxuICAgICAgLi4uKG5ldmVyUmVxdWlyZUFwcHJvdmFsID8gWyctLXJlcXVpcmUtYXBwcm92YWw9bmV2ZXInXSA6IFtdKSwgLy8gRGVmYXVsdCB0byBubyBhcHByb3ZhbCBpbiBhbiB1bmF0dGVuZGVkIHRlc3RcbiAgICAgIC4uLihvcHRpb25zLm9wdGlvbnMgPz8gW10pLFxuICAgICAgLi4udGhpcy5mdWxsU3RhY2tOYW1lKHN0YWNrTmFtZXMpXSwgb3B0aW9ucyk7XG4gIH1cblxuICBwdWJsaWMgYXN5bmMgY2RrRGVzdHJveShzdGFja05hbWVzOiBzdHJpbmcgfCBzdHJpbmdbXSwgb3B0aW9uczogQ2RrQ2xpT3B0aW9ucyA9IHt9KSB7XG4gICAgc3RhY2tOYW1lcyA9IHR5cGVvZiBzdGFja05hbWVzID09PSAnc3RyaW5nJyA/IFtzdGFja05hbWVzXSA6IHN0YWNrTmFtZXM7XG5cbiAgICByZXR1cm4gdGhpcy5jZGsoWydkZXN0cm95JyxcbiAgICAgICctZicsIC8vIFdlIG5ldmVyIHdhbnQgYSBwcm9tcHQgaW4gYW4gdW5hdHRlbmRlZCB0ZXN0XG4gICAgICAuLi4ob3B0aW9ucy5vcHRpb25zID8/IFtdKSxcbiAgICAgIC4uLnRoaXMuZnVsbFN0YWNrTmFtZShzdGFja05hbWVzKV0sIG9wdGlvbnMpO1xuICB9XG5cbiAgcHVibGljIGFzeW5jIGNkayhhcmdzOiBzdHJpbmdbXSwgb3B0aW9uczogQ2RrQ2xpT3B0aW9ucyA9IHt9KSB7XG4gICAgY29uc3QgdmVyYm9zZSA9IG9wdGlvbnMudmVyYm9zZSA/PyB0cnVlO1xuXG4gICAgcmV0dXJuIHRoaXMuc2hlbGwoWydjZGsnLCAuLi4odmVyYm9zZSA/IFsnLXYnXSA6IFtdKSwgLi4uYXJnc10sIHtcbiAgICAgIC4uLm9wdGlvbnMsXG4gICAgICBtb2RFbnY6IHtcbiAgICAgICAgQVdTX1JFR0lPTjogdGhpcy5hd3MucmVnaW9uLFxuICAgICAgICBBV1NfREVGQVVMVF9SRUdJT046IHRoaXMuYXdzLnJlZ2lvbixcbiAgICAgICAgU1RBQ0tfTkFNRV9QUkVGSVg6IHRoaXMuc3RhY2tOYW1lUHJlZml4LFxuICAgICAgICAuLi5vcHRpb25zLm1vZEVudixcbiAgICAgIH0sXG4gICAgfSk7XG4gIH1cblxuICBwdWJsaWMgZnVsbFN0YWNrTmFtZShzdGFja05hbWU6IHN0cmluZyk6IHN0cmluZztcbiAgcHVibGljIGZ1bGxTdGFja05hbWUoc3RhY2tOYW1lczogc3RyaW5nW10pOiBzdHJpbmdbXTtcbiAgcHVibGljIGZ1bGxTdGFja05hbWUoc3RhY2tOYW1lczogc3RyaW5nIHwgc3RyaW5nW10pOiBzdHJpbmcgfCBzdHJpbmdbXSB7XG4gICAgaWYgKHR5cGVvZiBzdGFja05hbWVzID09PSAnc3RyaW5nJykge1xuICAgICAgcmV0dXJuIGAke3RoaXMuc3RhY2tOYW1lUHJlZml4fS0ke3N0YWNrTmFtZXN9YDtcbiAgICB9IGVsc2Uge1xuICAgICAgcmV0dXJuIHN0YWNrTmFtZXMubWFwKHMgPT4gYCR7dGhpcy5zdGFja05hbWVQcmVmaXh9LSR7c31gKTtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogQXBwZW5kIHRoaXMgdG8gdGhlIGxpc3Qgb2YgYnVja2V0cyB0byBwb3RlbnRpYWxseSBkZWxldGVcbiAgICpcbiAgICogQXQgdGhlIGVuZCBvZiBhIHRlc3QsIHdlIGNsZWFuIHVwIGJ1Y2tldHMgdGhhdCBtYXkgbm90IGhhdmUgZ290dGVuIGRlc3Ryb3llZFxuICAgKiAoZm9yIHdoYXRldmVyIHJlYXNvbikuXG4gICAqL1xuICBwdWJsaWMgcmVtZW1iZXJUb0RlbGV0ZUJ1Y2tldChidWNrZXROYW1lOiBzdHJpbmcpIHtcbiAgICB0aGlzLmJ1Y2tldHNUb0RlbGV0ZS5wdXNoKGJ1Y2tldE5hbWUpO1xuICB9XG5cbiAgLyoqXG4gICAqIENsZWFudXAgbGVmdG92ZXIgc3RhY2tzIGFuZCBidWNrZXRzXG4gICAqL1xuICBwdWJsaWMgYXN5bmMgZGlzcG9zZShzdWNjZXNzOiBib29sZWFuKSB7XG4gICAgY29uc3Qgc3RhY2tzVG9EZWxldGUgPSBhd2FpdCB0aGlzLmRlbGV0ZWFibGVTdGFja3ModGhpcy5zdGFja05hbWVQcmVmaXgpO1xuXG4gICAgLy8gQm9vdHN0cmFwIHN0YWNrcyBoYXZlIGJ1Y2tldHMgdGhhdCBuZWVkIHRvIGJlIGNsZWFuZWRcbiAgICBjb25zdCBidWNrZXROYW1lcyA9IHN0YWNrc1RvRGVsZXRlLm1hcChzdGFjayA9PiBvdXRwdXRGcm9tU3RhY2soJ0J1Y2tldE5hbWUnLCBzdGFjaykpLmZpbHRlcihkZWZpbmVkKTtcbiAgICBhd2FpdCBQcm9taXNlLmFsbChidWNrZXROYW1lcy5tYXAoYiA9PiB0aGlzLmF3cy5lbXB0eUJ1Y2tldChiKSkpO1xuXG4gICAgLy8gQm9vdHN0cmFwIHN0YWNrcyBoYXZlIEVDUiByZXBvc2l0b3JpZXMgd2l0aCBpbWFnZXMgd2hpY2ggc2hvdWxkIGJlIGRlbGV0ZWRcbiAgICBjb25zdCBpbWFnZVJlcG9zaXRvcnlOYW1lcyA9IHN0YWNrc1RvRGVsZXRlLm1hcChzdGFjayA9PiBvdXRwdXRGcm9tU3RhY2soJ0ltYWdlUmVwb3NpdG9yeU5hbWUnLCBzdGFjaykpLmZpbHRlcihkZWZpbmVkKTtcbiAgICBhd2FpdCBQcm9taXNlLmFsbChpbWFnZVJlcG9zaXRvcnlOYW1lcy5tYXAociA9PiB0aGlzLmF3cy5kZWxldGVJbWFnZVJlcG9zaXRvcnkocikpKTtcblxuICAgIGF3YWl0IHRoaXMuYXdzLmRlbGV0ZVN0YWNrcyguLi5zdGFja3NUb0RlbGV0ZS5tYXAocyA9PiBzLlN0YWNrTmFtZSkpO1xuXG4gICAgLy8gV2UgbWlnaHQgaGF2ZSBsZWFrZWQgc29tZSBidWNrZXRzIGJ5IHVwZ3JhZGluZyB0aGUgYm9vdHN0cmFwIHN0YWNrLiBCZVxuICAgIC8vIHN1cmUgdG8gY2xlYW4gZXZlcnl0aGluZy5cbiAgICBmb3IgKGNvbnN0IGJ1Y2tldCBvZiB0aGlzLmJ1Y2tldHNUb0RlbGV0ZSkge1xuICAgICAgYXdhaXQgdGhpcy5hd3MuZGVsZXRlQnVja2V0KGJ1Y2tldCk7XG4gICAgfVxuXG4gICAgLy8gSWYgdGhlIHRlc3RzIGNvbXBsZXRlZCBzdWNjZXNzZnVsbHksIGhhcHBpbHkgZGVsZXRlIHRoZSBmaXh0dXJlXG4gICAgLy8gKG90aGVyd2lzZSBsZWF2ZSBpdCBmb3IgaHVtYW5zIHRvIGluc3BlY3QpXG4gICAgaWYgKHN1Y2Nlc3MpIHtcbiAgICAgIHJpbXJhZih0aGlzLmludGVnVGVzdERpcik7XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIFJldHVybiB0aGUgc3RhY2tzIHN0YXJ0aW5nIHdpdGggb3VyIHRlc3RpbmcgcHJlZml4IHRoYXQgc2hvdWxkIGJlIGRlbGV0ZWRcbiAgICovXG4gIHByaXZhdGUgYXN5bmMgZGVsZXRlYWJsZVN0YWNrcyhwcmVmaXg6IHN0cmluZyk6IFByb21pc2U8QVdTLkNsb3VkRm9ybWF0aW9uLlN0YWNrW10+IHtcbiAgICBjb25zdCBzdGF0dXNGaWx0ZXIgPSBbXG4gICAgICAnQ1JFQVRFX0lOX1BST0dSRVNTJywgJ0NSRUFURV9GQUlMRUQnLCAnQ1JFQVRFX0NPTVBMRVRFJyxcbiAgICAgICdST0xMQkFDS19JTl9QUk9HUkVTUycsICdST0xMQkFDS19GQUlMRUQnLCAnUk9MTEJBQ0tfQ09NUExFVEUnLFxuICAgICAgJ0RFTEVURV9GQUlMRUQnLFxuICAgICAgJ1VQREFURV9JTl9QUk9HUkVTUycsICdVUERBVEVfQ09NUExFVEVfQ0xFQU5VUF9JTl9QUk9HUkVTUycsXG4gICAgICAnVVBEQVRFX0NPTVBMRVRFJywgJ1VQREFURV9ST0xMQkFDS19JTl9QUk9HUkVTUycsXG4gICAgICAnVVBEQVRFX1JPTExCQUNLX0ZBSUxFRCcsXG4gICAgICAnVVBEQVRFX1JPTExCQUNLX0NPTVBMRVRFX0NMRUFOVVBfSU5fUFJPR1JFU1MnLFxuICAgICAgJ1VQREFURV9ST0xMQkFDS19DT01QTEVURScsICdSRVZJRVdfSU5fUFJPR1JFU1MnLFxuICAgICAgJ0lNUE9SVF9JTl9QUk9HUkVTUycsICdJTVBPUlRfQ09NUExFVEUnLFxuICAgICAgJ0lNUE9SVF9ST0xMQkFDS19JTl9QUk9HUkVTUycsICdJTVBPUlRfUk9MTEJBQ0tfRkFJTEVEJyxcbiAgICAgICdJTVBPUlRfUk9MTEJBQ0tfQ09NUExFVEUnLFxuICAgIF07XG5cbiAgICBjb25zdCByZXNwb25zZSA9IGF3YWl0IHRoaXMuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHt9KTtcblxuICAgIHJldHVybiAocmVzcG9uc2UuU3RhY2tzID8/IFtdKVxuICAgICAgLmZpbHRlcihzID0+IHMuU3RhY2tOYW1lLnN0YXJ0c1dpdGgocHJlZml4KSlcbiAgICAgIC5maWx0ZXIocyA9PiBzdGF0dXNGaWx0ZXIuaW5jbHVkZXMocy5TdGFja1N0YXR1cykpXG4gICAgICAuZmlsdGVyKHMgPT4gcy5Sb290SWQgPT09IHVuZGVmaW5lZCk7IC8vIE9ubHkgZGVsZXRlIHBhcmVudCBzdGFja3MuIE5lc3RlZCBzdGFja3MgYXJlIGRlbGV0ZWQgaW4gdGhlIHByb2Nlc3NcbiAgfVxufVxuXG4vKipcbiAqIFBlcmZvcm0gYSBvbmUtdGltZSBxdWljayBzYW5pdHkgY2hlY2sgdGhhdCB0aGUgQVdTIGNsaWVudHMgaGFzIHByb3Blcmx5IGNvbmZpZ3VyZWQgY3JlZGVudGlhbHNcbiAqXG4gKiBJZiB3ZSBkb24ndCBkbyB0aGlzLCBjYWxscyBhcmUgZ29pbmcgdG8gZmFpbCBhbmQgdGhleSdsbCBiZSByZXRyaWVkIGFuZCBldmVyeXRoaW5nIHdpbGwgdGFrZVxuICogZm9yZXZlciBiZWZvcmUgdGhlIHVzZXIgbm90aWNlcyBhIHNpbXBsZSBtaXNjb25maWd1cmF0aW9uLlxuICpcbiAqIFdlIGNhbid0IGNoZWNrIGZvciB0aGUgcHJlc2VuY2Ugb2YgZW52aXJvbm1lbnQgdmFyaWFibGVzIHNpbmNlIGNyZWRlbnRpYWxzIGNvdWxkIGNvbWUgZnJvbVxuICogYW55d2hlcmUsIHNvIGRvIHNpbXBsZSBhY2NvdW50IHJldHJpZXZhbC5cbiAqXG4gKiBPbmx5IGRvIGl0IG9uY2UgcGVyIHByb2Nlc3MuXG4gKi9cbmFzeW5jIGZ1bmN0aW9uIHNhbml0eUNoZWNrKGF3czogQXdzQ2xpZW50cykge1xuICBpZiAoc2FuaXR5Q2hlY2tlZCA9PT0gdW5kZWZpbmVkKSB7XG4gICAgdHJ5IHtcbiAgICAgIGF3YWl0IGF3cy5hY2NvdW50KCk7XG4gICAgICBzYW5pdHlDaGVja2VkID0gdHJ1ZTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICBzYW5pdHlDaGVja2VkID0gZmFsc2U7XG4gICAgICB0aHJvdyBuZXcgRXJyb3IoYEFXUyBjcmVkZW50aWFscyBwcm9iYWJseSBub3QgY29uZmlndXJlZCwgZ290IGVycm9yOiAke2UubWVzc2FnZX1gKTtcbiAgICB9XG4gIH1cbiAgaWYgKCFzYW5pdHlDaGVja2VkKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdBV1MgY3JlZGVudGlhbHMgcHJvYmFibHkgbm90IGNvbmZpZ3VyZWQsIHNlZSBwcmV2aW91cyBlcnJvcicpO1xuICB9XG59XG5sZXQgc2FuaXR5Q2hlY2tlZDogYm9vbGVhbiB8IHVuZGVmaW5lZDtcblxuLyoqXG4gKiBNYWtlIHN1cmUgdGhhdCB0aGUgZ2l2ZW4gZW52aXJvbm1lbnQgaXMgYm9vdHN0cmFwcGVkXG4gKlxuICogU2luY2Ugd2UgZ28gc3RyaXBpbmcgYWNyb3NzIHJlZ2lvbnMsIGl0J3MgZ29pbmcgdG8gc3VjayBkb2luZyB0aGlzXG4gKiBieSBoYW5kIHNvIGxldCdzIGp1c3QgbWFzcy1hdXRvbWF0ZSBpdC5cbiAqL1xuYXN5bmMgZnVuY3Rpb24gZW5zdXJlQm9vdHN0cmFwcGVkKGZpeHR1cmU6IFRlc3RGaXh0dXJlKSB7XG4gIC8vIE9sZC1zdHlsZSBib290c3RyYXAgc3RhY2sgd2l0aCBkZWZhdWx0IG5hbWVcbiAgaWYgKGF3YWl0IGZpeHR1cmUuYXdzLnN0YWNrU3RhdHVzKCdDREtUb29sa2l0JykgPT09IHVuZGVmaW5lZCkge1xuICAgIGF3YWl0IGZpeHR1cmUuY2RrKFsnYm9vdHN0cmFwJywgYGF3czovLyR7YXdhaXQgZml4dHVyZS5hd3MuYWNjb3VudCgpfS8ke2ZpeHR1cmUuYXdzLnJlZ2lvbn1gXSk7XG4gIH1cbn1cblxuLyoqXG4gKiBBIHNoZWxsIGNvbW1hbmQgdGhhdCBkb2VzIHdoYXQgeW91IHdhbnRcbiAqXG4gKiBJcyBwbGF0Zm9ybS1hd2FyZSwgaGFuZGxlcyBlcnJvcnMgbmljZWx5LlxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gc2hlbGwoY29tbWFuZDogc3RyaW5nW10sIG9wdGlvbnM6IFNoZWxsT3B0aW9ucyA9IHt9KTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgaWYgKG9wdGlvbnMubW9kRW52ICYmIG9wdGlvbnMuZW52KSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdVc2UgZWl0aGVyIGVudiBvciBtb2RFbnYgYnV0IG5vdCBib3RoJyk7XG4gIH1cblxuICBvcHRpb25zLm91dHB1dD8ud3JpdGUoYPCfkrsgJHtjb21tYW5kLmpvaW4oJyAnKX1cXG5gKTtcblxuICBjb25zdCBlbnYgPSBvcHRpb25zLmVudiA/PyAob3B0aW9ucy5tb2RFbnYgPyB7IC4uLnByb2Nlc3MuZW52LCAuLi5vcHRpb25zLm1vZEVudiB9IDogdW5kZWZpbmVkKTtcblxuICBjb25zdCBjaGlsZCA9IGNoaWxkX3Byb2Nlc3Muc3Bhd24oY29tbWFuZFswXSwgY29tbWFuZC5zbGljZSgxKSwge1xuICAgIC4uLm9wdGlvbnMsXG4gICAgZW52LFxuICAgIC8vIE5lZWQgdGhpcyBmb3IgV2luZG93cyB3aGVyZSB3ZSB3YW50IC5jbWQgYW5kIC5iYXQgdG8gYmUgZm91bmQgYXMgd2VsbC5cbiAgICBzaGVsbDogdHJ1ZSxcbiAgICBzdGRpbzogWydpZ25vcmUnLCAncGlwZScsICdwaXBlJ10sXG4gIH0pO1xuXG4gIHJldHVybiBuZXcgUHJvbWlzZTxzdHJpbmc+KChyZXNvbHZlLCByZWplY3QpID0+IHtcbiAgICBjb25zdCBzdGRvdXQgPSBuZXcgQXJyYXk8QnVmZmVyPigpO1xuICAgIGNvbnN0IHN0ZGVyciA9IG5ldyBBcnJheTxCdWZmZXI+KCk7XG5cbiAgICBjaGlsZC5zdGRvdXQhLm9uKCdkYXRhJywgY2h1bmsgPT4ge1xuICAgICAgb3B0aW9ucy5vdXRwdXQ/LndyaXRlKGNodW5rKTtcbiAgICAgIHN0ZG91dC5wdXNoKGNodW5rKTtcbiAgICB9KTtcblxuICAgIGNoaWxkLnN0ZGVyciEub24oJ2RhdGEnLCBjaHVuayA9PiB7XG4gICAgICBvcHRpb25zLm91dHB1dD8ud3JpdGUoY2h1bmspO1xuICAgICAgaWYgKG9wdGlvbnMuY2FwdHVyZVN0ZGVyciA/PyB0cnVlKSB7XG4gICAgICAgIHN0ZGVyci5wdXNoKGNodW5rKTtcbiAgICAgIH1cbiAgICB9KTtcblxuICAgIGNoaWxkLm9uY2UoJ2Vycm9yJywgcmVqZWN0KTtcblxuICAgIGNoaWxkLm9uY2UoJ2Nsb3NlJywgY29kZSA9PiB7XG4gICAgICBpZiAoY29kZSA9PT0gMCB8fCBvcHRpb25zLmFsbG93RXJyRXhpdCkge1xuICAgICAgICByZXNvbHZlKChCdWZmZXIuY29uY2F0KHN0ZG91dCkudG9TdHJpbmcoJ3V0Zi04JykgKyBCdWZmZXIuY29uY2F0KHN0ZGVycikudG9TdHJpbmcoJ3V0Zi04JykpLnRyaW0oKSk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICByZWplY3QobmV3IEVycm9yKGAnJHtjb21tYW5kLmpvaW4oJyAnKX0nIGV4aXRlZCB3aXRoIGVycm9yIGNvZGUgJHtjb2RlfWApKTtcbiAgICAgIH1cbiAgICB9KTtcbiAgfSk7XG59XG5cbmZ1bmN0aW9uIGRlZmluZWQ8QT4oeDogQSk6IHggaXMgTm9uTnVsbGFibGU8QT4ge1xuICByZXR1cm4geCAhPT0gdW5kZWZpbmVkO1xufVxuXG4vKipcbiAqIHJtIC1yZiByZWltcGxlbWVudGF0aW9uLCBkb24ndCB3YW50IHRvIGRlcGVuZCBvbiBhbiBOUE0gcGFja2FnZSBmb3IgdGhpc1xuICovXG5leHBvcnQgZnVuY3Rpb24gcmltcmFmKGZzUGF0aDogc3RyaW5nKSB7XG4gIHRyeSB7XG4gICAgY29uc3QgaXNEaXIgPSBmcy5sc3RhdFN5bmMoZnNQYXRoKS5pc0RpcmVjdG9yeSgpO1xuXG4gICAgaWYgKGlzRGlyKSB7XG4gICAgICBmb3IgKGNvbnN0IGZpbGUgb2YgZnMucmVhZGRpclN5bmMoZnNQYXRoKSkge1xuICAgICAgICByaW1yYWYocGF0aC5qb2luKGZzUGF0aCwgZmlsZSkpO1xuICAgICAgfVxuICAgICAgZnMucm1kaXJTeW5jKGZzUGF0aCk7XG4gICAgfSBlbHNlIHtcbiAgICAgIGZzLnVubGlua1N5bmMoZnNQYXRoKTtcbiAgICB9XG4gIH0gY2F0Y2ggKGUpIHtcbiAgICAvLyBXZSB3aWxsIHN1cnZpdmUgRU5PRU5UXG4gICAgaWYgKGUuY29kZSAhPT0gJ0VOT0VOVCcpIHsgdGhyb3cgZTsgfVxuICB9XG59XG5cbmV4cG9ydCBmdW5jdGlvbiByYW5kb21TdHJpbmcoKSB7XG4gIC8vIENyYXp5XG4gIHJldHVybiBNYXRoLnJhbmRvbSgpLnRvU3RyaW5nKDM2KS5yZXBsYWNlKC9bXmEtejAtOV0rL2csICcnKTtcbn0iXX0= \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.0/cli.integtest.js b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.0/cli.integtest.js new file mode 100644 index 000000000..a63578ecf --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.0/cli.integtest.js @@ -0,0 +1,599 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = require("fs"); +const os = require("os"); +const path = require("path"); +const aws_helpers_1 = require("./aws-helpers"); +const cdk_helpers_1 = require("./cdk-helpers"); +const test_helpers_1 = require("./test-helpers"); +jest.setTimeout(600 * 1000); +test_helpers_1.integTest('VPC Lookup', cdk_helpers_1.withDefaultFixture(async (fixture) => { + fixture.log('Making sure we are clean before starting.'); + await fixture.cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setting up: creating a VPC with known tags'); + await fixture.cdkDeploy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setup complete!'); + fixture.log('Verifying we can now import that VPC'); + await fixture.cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' } }); +})); +test_helpers_1.integTest('Two ways of shoing the version', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const version1 = await fixture.cdk(['version'], { verbose: false }); + const version2 = await fixture.cdk(['--version'], { verbose: false }); + expect(version1).toEqual(version2); +})); +test_helpers_1.integTest('Termination protection', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const stackName = 'termination-protection'; + await fixture.cdkDeploy(stackName); + // Try a destroy that should fail + await expect(fixture.cdkDestroy(stackName)).rejects.toThrow('exited with error'); + // Can update termination protection even though the change set doesn't contain changes + await fixture.cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); + await fixture.cdkDestroy(stackName); +})); +test_helpers_1.integTest('cdk synth', cdk_helpers_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdk(['synth', fixture.fullStackName('test-1')], { verbose: false })).resolves.toEqual(`Resources: + topic69831491: + Type: AWS::SNS::Topic + Metadata: + aws:cdk:path: ${fixture.stackNamePrefix}-test-1/topic/Resource`); + await expect(fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false })).resolves.toEqual(`Resources: + topic152D84A37: + Type: AWS::SNS::Topic + Metadata: + aws:cdk:path: ${fixture.stackNamePrefix}-test-2/topic1/Resource + topic2A4FB547F: + Type: AWS::SNS::Topic + Metadata: + aws:cdk:path: ${fixture.stackNamePrefix}-test-2/topic2/Resource`); +})); +test_helpers_1.integTest('ssm parameter provider error', cdk_helpers_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdk(['synth', + fixture.fullStackName('missing-ssm-parameter'), + '-c', 'test:ssm-parameter-name=/does/not/exist'], { + allowErrExit: true, + })).resolves.toContain('SSM parameter not available in account'); +})); +test_helpers_1.integTest('automatic ordering', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // Deploy the consuming stack which will include the producing stack + await fixture.cdkDeploy('order-consuming'); + // Destroy the providing stack which will include the consuming stack + await fixture.cdkDestroy('order-providing'); +})); +test_helpers_1.integTest('context setting', cdk_helpers_1.withDefaultFixture(async (fixture) => { + await fs_1.promises.writeFile(path.join(fixture.integTestDir, 'cdk.context.json'), JSON.stringify({ + contextkey: 'this is the context value', + })); + try { + await expect(fixture.cdk(['context'])).resolves.toContain('this is the context value'); + // Test that deleting the contextkey works + await fixture.cdk(['context', '--reset', 'contextkey']); + await expect(fixture.cdk(['context'])).resolves.not.toContain('this is the context value'); + // Test that forced delete of the context key does not throw + await fixture.cdk(['context', '-f', '--reset', 'contextkey']); + } + finally { + await fs_1.promises.unlink(path.join(fixture.integTestDir, 'cdk.context.json')); + } +})); +test_helpers_1.integTest('deploy', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('test-2', { captureStderr: false }); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(2); +})); +test_helpers_1.integTest('deploy all', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const arns = await fixture.cdkDeploy('test-*', { captureStderr: false }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(arns.split('\n').length).toEqual(2); +})); +test_helpers_1.integTest('nested stack with parameters', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances + // of this test to run in parallel, othewise they will attempt to create the same SNS topic. + const stackArn = await fixture.cdkDeploy('with-nested-stack-using-parameters', { + options: ['--parameters', `MyTopicParam=${fixture.stackNamePrefix}ThereIsNoSpoon`], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(1); +})); +test_helpers_1.integTest('deploy without execute', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--no-execute'], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); +})); +test_helpers_1.integTest('security related changes without a CLI are expected to fail', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // redirect /dev/null to stdin, which means there will not be tty attached + // since this stack includes security-related changes, the deployment should + // immediately fail because we can't confirm the changes + const stackName = 'iam-test'; + await expect(fixture.cdkDeploy(stackName, { + options: ['<', '/dev/null'], + neverRequireApproval: false, + })).rejects.toThrow('exited with error'); + // Ensure stack was not deployed + await expect(fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName(stackName), + })).rejects.toThrow('does not exist'); +})); +test_helpers_1.integTest('deploy wildcard with outputs', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs_1.promises.mkdir(path.dirname(outputsFile), { recursive: true }); + await fixture.cdkDeploy(['outputs-test-*'], { + options: ['--outputs-file', outputsFile], + }); + const outputs = JSON.parse((await fs_1.promises.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + expect(outputs).toEqual({ + [`${fixture.stackNamePrefix}-outputs-test-1`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-1MyTopic`, + }, + [`${fixture.stackNamePrefix}-outputs-test-2`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-2MyOtherTopic`, + }, + }); +})); +test_helpers_1.integTest('deploy with parameters', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}bazinga`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}bazinga`, + }, + ]); +})); +test_helpers_1.integTest('update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('param-test-1'), + }); + const stackArn = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackId; + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('ROLLBACK_COMPLETE'); + // WHEN + const newStackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + const newStackResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: newStackArn, + }); + // THEN + expect(stackArn).not.toEqual(newStackArn); // new stack was created + expect((_c = newStackResponse.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('CREATE_COMPLETE'); + expect((_d = newStackResponse.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('stack in UPDATE_ROLLBACK_COMPLETE state can be updated', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}nice`, + ], + captureStderr: false, + }); + let response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('CREATE_COMPLETE'); + // bad parameter name with @ will put stack into UPDATE_ROLLBACK_COMPLETE + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + ; + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('UPDATE_ROLLBACK_COMPLETE'); + // WHEN + await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + // THEN + expect((_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('UPDATE_COMPLETE'); + expect((_d = response.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('deploy with wildcard and parameters', cdk_helpers_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('param-test-*', { + options: [ + '--parameters', `${fixture.stackNamePrefix}-param-test-1:TopicNameParam=${fixture.stackNamePrefix}bazinga`, + '--parameters', `${fixture.stackNamePrefix}-param-test-2:OtherTopicNameParam=${fixture.stackNamePrefix}ThatsMySpot`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:DisplayNameParam=${fixture.stackNamePrefix}HeyThere`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:OtherDisplayNameParam=${fixture.stackNamePrefix}AnotherOne`, + ], + }); +})); +test_helpers_1.integTest('deploy with parameters multi', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + const paramVal1 = `${fixture.stackNamePrefix}bazinga`; + const paramVal2 = `${fixture.stackNamePrefix}=jagshemash`; + const stackArn = await fixture.cdkDeploy('param-test-3', { + options: [ + '--parameters', `DisplayNameParam=${paramVal1}`, + '--parameters', `OtherDisplayNameParam=${paramVal2}`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'DisplayNameParam', + ParameterValue: paramVal1, + }, + { + ParameterKey: 'OtherDisplayNameParam', + ParameterValue: paramVal2, + }, + ]); +})); +test_helpers_1.integTest('deploy with notification ARN', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + const topicName = `${fixture.stackNamePrefix}-test-topic`; + const response = await fixture.aws.sns('createTopic', { Name: topicName }); + const topicArn = response.TopicArn; + try { + await fixture.cdkDeploy('test-2', { + options: ['--notification-arns', topicArn], + }); + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('test-2'), + }); + expect((_a = describeResponse.Stacks) === null || _a === void 0 ? void 0 : _a[0].NotificationARNs).toEqual([topicArn]); + } + finally { + await fixture.aws.sns('deleteTopic', { + TopicArn: topicArn, + }); + } +})); +test_helpers_1.integTest('deploy with role', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const roleName = `${fixture.stackNamePrefix}-test-role`; + await deleteRole(); + const createResponse = await fixture.aws.iam('createRole', { + RoleName: roleName, + AssumeRolePolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: 'sts:AssumeRole', + Principal: { Service: 'cloudformation.amazonaws.com' }, + Effect: 'Allow', + }, { + Action: 'sts:AssumeRole', + Principal: { AWS: (await fixture.aws.sts('getCallerIdentity', {})).Arn }, + Effect: 'Allow', + }], + }), + }); + const roleArn = createResponse.Role.Arn; + try { + await fixture.aws.iam('putRolePolicy', { + RoleName: roleName, + PolicyName: 'DefaultPolicy', + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: '*', + Resource: '*', + Effect: 'Allow', + }], + }), + }); + await aws_helpers_1.retry(fixture.output, 'Trying to assume fresh role', aws_helpers_1.retry.forSeconds(300), async () => { + await fixture.aws.sts('assumeRole', { + RoleArn: roleArn, + RoleSessionName: 'testing', + }); + }); + // In principle, the role has replicated from 'us-east-1' to wherever we're testing. + // Give it a little more sleep to make sure CloudFormation is not hitting a box + // that doesn't have it yet. + await aws_helpers_1.sleep(5000); + await fixture.cdkDeploy('test-2', { + options: ['--role-arn', roleArn], + }); + // Immediately delete the stack again before we delete the role. + // + // Since roles are sticky, if we delete the role before the stack, subsequent DeleteStack + // operations will fail when CloudFormation tries to assume the role that's already gone. + await fixture.cdkDestroy('test-2'); + } + finally { + await deleteRole(); + } + async function deleteRole() { + try { + for (const policyName of (await fixture.aws.iam('listRolePolicies', { RoleName: roleName })).PolicyNames) { + await fixture.aws.iam('deleteRolePolicy', { + RoleName: roleName, + PolicyName: policyName, + }); + } + await fixture.aws.iam('deleteRole', { RoleName: roleName }); + } + catch (e) { + if (e.message.indexOf('cannot be found') > -1) { + return; + } + throw e; + } + } +})); +test_helpers_1.integTest('cdk diff', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // We can make it fail by passing --fail + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1')])) + .rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail on multiple stacks exits with error if any of the stacks contains a diff', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // GIVEN + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + await fixture.cdkDeploy('test-2'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('There were no differences'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail with multiple stack exits with if any of the stacks contains a diff', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('test-1'); + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('There were no differences'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('deploy stack with docker asset', cdk_helpers_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('docker'); +})); +test_helpers_1.integTest('deploy and test stack with lambda asset', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a, _b; + const stackArn = await fixture.cdkDeploy('lambda', { captureStderr: false }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + const lambdaArn = (_b = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Outputs) === null || _b === void 0 ? void 0 : _b[0].OutputValue; + if (lambdaArn === undefined) { + throw new Error('Stack did not have expected Lambda ARN output'); + } + const output = await fixture.aws.lambda('invoke', { + FunctionName: lambdaArn, + }); + expect(JSON.stringify(output.Payload)).toContain('dear asset'); +})); +test_helpers_1.integTest('cdk ls', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls'], { captureStderr: false }); + const expectedStacks = [ + 'conditional-resource', + 'docker', + 'docker-with-custom-file', + 'failed', + 'iam-test', + 'lambda', + 'missing-ssm-parameter', + 'order-providing', + 'outputs-test-1', + 'outputs-test-2', + 'param-test-1', + 'param-test-2', + 'param-test-3', + 'termination-protection', + 'test-1', + 'test-2', + 'with-nested-stack', + 'with-nested-stack-using-parameters', + 'order-consuming', + ]; + for (const stack of expectedStacks) { + expect(listing).toContain(fixture.fullStackName(stack)); + } +})); +test_helpers_1.integTest('deploy stack without resource', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // Deploy the stack without resources + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + // This should have succeeded but not deployed the stack. + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); + // Deploy the stack with resources + await fixture.cdkDeploy('conditional-resource'); + // Then again WITHOUT resources (this should destroy the stack) + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); +})); +test_helpers_1.integTest('IAM diff', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]); + // Roughly check for a table like this: + // + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ + // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ + expect(output).toContain('${SomeRole.Arn}'); + expect(output).toContain('sts:AssumeRole'); + expect(output).toContain('ec2.amazonaws.com'); +})); +test_helpers_1.integTest('fast deploy', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // we are using a stack with a nested stack because CFN will always attempt to + // update a nested stack, which will allow us to verify that updates are actually + // skipped unless --force is specified. + const stackArn = await fixture.cdkDeploy('with-nested-stack', { captureStderr: false }); + const changeSet1 = await getLatestChangeSet(); + // Deploy the same stack again, there should be no new change set created + await fixture.cdkDeploy('with-nested-stack'); + const changeSet2 = await getLatestChangeSet(); + expect(changeSet2.ChangeSetId).toEqual(changeSet1.ChangeSetId); + // Deploy the stack again with --force, now we should create a changeset + await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] }); + const changeSet3 = await getLatestChangeSet(); + expect(changeSet3.ChangeSetId).not.toEqual(changeSet2.ChangeSetId); + // Deploy the stack again with tags, expected to create a new changeset + // even though the resources didn't change. + await fixture.cdkDeploy('with-nested-stack', { options: ['--tags', 'key=value'] }); + const changeSet4 = await getLatestChangeSet(); + expect(changeSet4.ChangeSetId).not.toEqual(changeSet3.ChangeSetId); + async function getLatestChangeSet() { + var _a, _b, _c; + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn }); + if (!((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0])) { + throw new Error('Did not get a ChangeSet at all'); + } + fixture.log(`Found Change Set ${(_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].ChangeSetId}`); + return (_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0]; + } +})); +test_helpers_1.integTest('failed deploy does not hang', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. + await expect(fixture.cdkDeploy('failed')).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('can still load old assemblies', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx'); + const testAssembliesDirectory = path.join(__dirname, 'cloud-assemblies'); + for (const asmdir of await listChildDirs(testAssembliesDirectory)) { + fixture.log(`ASSEMBLY ${asmdir}`); + await cdk_helpers_1.cloneDirectory(asmdir, cxAsmDir); + // Some files in the asm directory that have a .js extension are + // actually treated as templates. Evaluate them using NodeJS. + const templates = await listChildren(cxAsmDir, fullPath => Promise.resolve(fullPath.endsWith('.js'))); + for (const template of templates) { + const targetName = template.replace(/.js$/, ''); + await cdk_helpers_1.shell([process.execPath, template, '>', targetName], { + cwd: cxAsmDir, + output: fixture.output, + modEnv: { + TEST_ACCOUNT: await fixture.aws.account(), + TEST_REGION: fixture.aws.region, + }, + }); + } + // Use this directory as a Cloud Assembly + const output = await fixture.cdk([ + '--app', cxAsmDir, + '-v', + 'synth', + ]); + // Assert that there was no providerError in CDK's stderr + // Because we rely on the app/framework to actually error in case the + // provider fails, we inspect the logs here. + expect(output).not.toContain('$providerError'); + } +})); +test_helpers_1.integTest('generating and loading assembly', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`; + await fixture.shell(['rm', '-rf', asmOutputDir]); + // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory. + await fixture.cdk(['synth']); + await fixture.cdk(['synth', '--output', asmOutputDir]); + // cdk.out in the current directory and the indicated --output should be the same + await fixture.shell(['diff', 'cdk.out', asmOutputDir]); + // Check that we can 'ls' the synthesized asm. + // Change to some random directory to make sure we're not accidentally loading cdk.json + const list = await fixture.cdk(['--app', asmOutputDir, 'ls'], { cwd: os.tmpdir() }); + // Same stacks we know are in the app + expect(list).toContain(`${fixture.stackNamePrefix}-lambda`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-1`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-2`); + // Check that we can use '.' and just synth ,the generated asm + const stackTemplate = await fixture.cdk(['--app', '.', 'synth', fixture.fullStackName('test-2')], { + cwd: asmOutputDir, + }); + expect(stackTemplate).toContain('topic152D84A37'); + // Deploy a Lambda from the copied asm + await fixture.cdkDeploy('lambda', { options: ['-a', '.'], cwd: asmOutputDir }); + // Remove (rename) the original custom docker file that was used during synth. + // this verifies that the assemly has a copy of it and that the manifest uses + // relative paths to reference to it. + const customDockerFile = path.join(fixture.integTestDir, 'docker', 'Dockerfile.Custom'); + await fs_1.promises.rename(customDockerFile, `${customDockerFile}~`); + try { + // deploy a docker image with custom file without synth (uses assets) + await fixture.cdkDeploy('docker-with-custom-file', { options: ['-a', '.'], cwd: asmOutputDir }); + } + finally { + // Rename back to restore fixture to original state + await fs_1.promises.rename(`${customDockerFile}~`, customDockerFile); + } +})); +test_helpers_1.integTest('templates on disk contain metadata resource, also in nested assemblies', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // Synth first, and switch on version reporting because cdk.json is disabling it + await fixture.cdk(['synth', '--version-reporting=true']); + // Load template from disk from root assembly + const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']); + expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); + // Load template from nested assembly + const nestedTemplateContents = await fixture.shell(['cat', 'cdk.out/assembly-*-stage/*-stage-StackInStage.template.json']); + expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); +})); +async function listChildren(parent, pred) { + const ret = new Array(); + for (const child of await fs_1.promises.readdir(parent, { encoding: 'utf-8' })) { + const fullPath = path.join(parent, child.toString()); + if (await pred(fullPath)) { + ret.push(fullPath); + } + } + return ret; +} +async function listChildDirs(parent) { + return listChildren(parent, async (fullPath) => (await fs_1.promises.stat(fullPath)).isDirectory()); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLmludGVndGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNsaS5pbnRlZ3Rlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSwyQkFBb0M7QUFDcEMseUJBQXlCO0FBQ3pCLDZCQUE2QjtBQUM3QiwrQ0FBNkM7QUFDN0MsK0NBQTBFO0FBQzFFLGlEQUEyQztBQUUzQyxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsR0FBRyxJQUFJLENBQUMsQ0FBQztBQUU1Qix3QkFBUyxDQUFDLFlBQVksRUFBRSxnQ0FBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDM0QsT0FBTyxDQUFDLEdBQUcsQ0FBQywyQ0FBMkMsQ0FBQyxDQUFDO0lBQ3pELE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFFckYsT0FBTyxDQUFDLEdBQUcsQ0FBQyw0Q0FBNEMsQ0FBQyxDQUFDO0lBQzFELE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDcEYsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRS9CLE9BQU8sQ0FBQyxHQUFHLENBQUMsc0NBQXNDLENBQUMsQ0FBQztJQUNwRCxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsWUFBWSxFQUFFLEVBQUUsTUFBTSxFQUFFLEVBQUUsa0JBQWtCLEVBQUUsUUFBUSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBQ3RGLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGdDQUFnQyxFQUFFLGdDQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUMvRSxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFH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LENBQUMsQ0FBQyxDQUFDO0lBRTNILE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLHNCQUFzQixDQUFDLENBQUMsU0FBUyxDQUFDLFdBQVcsQ0FBQyxDQUFDLFVBQVUsRUFBRSxDQUFDO0FBQ2hGLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSixLQUFLLFVBQVUsWUFBWSxDQUFDLE1BQWMsRUFBRSxJQUFxQztJQUMvRSxNQUFNLEdBQUcsR0FBRyxJQUFJLEtBQUssRUFBVSxDQUFDO0lBQ2hDLEtBQUssTUFBTSxLQUFLLElBQUksTUFBTSxhQUFFLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxFQUFFO1FBQ25FLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLEtBQUssQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO1FBQ3JELElBQUksTUFBTSxJQUFJLENBQUMsUUFBUSxDQUFDLEVBQUU7WUFDeEIsR0FBRyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQztTQUNwQjtLQUNGO0lBQ0QsT0FBTyxHQUFHLENBQUM7QUFDYixDQUFDO0FBRUQsS0FBSyxVQUFVLGFBQWEsQ0FBQyxNQUFjO0lBQ3pDLE9BQU8sWUFBWSxDQUFDLE1BQU0sRUFBRSxLQUFLLEVBQUUsUUFBZ0IsRUFBRSxFQUFFLENBQUMsQ0FBQyxNQUFNLGFBQUUsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO0FBQ25HLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBwcm9taXNlcyBhcyBmcyB9IGZyb20gJ2ZzJztcbmltcG9ydCAqIGFzIG9zIGZyb20gJ29zJztcbmltcG9ydCAqIGFzIHBhdGggZnJvbSAncGF0aCc7XG5pbXBvcnQgeyByZXRyeSwgc2xlZXAgfSBmcm9tICcuL2F3cy1oZWxwZXJzJztcbmltcG9ydCB7IGNsb25lRGlyZWN0b3J5LCBzaGVsbCwgd2l0aERlZmF1bHRGaXh0dXJlIH0gZnJvbSAnLi9jZGstaGVscGVycyc7XG5pbXBvcnQgeyBpbnRlZ1Rlc3QgfSBmcm9tICcuL3Rlc3QtaGVscGVycyc7XG5cbmplc3Quc2V0VGltZW91dCg2MDAgKiAxMDAwKTtcblxuaW50ZWdUZXN0KCdWUEMgTG9va3VwJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGZpeHR1cmUubG9nKCdNYWtpbmcgc3VyZSB3ZSBhcmUgY2xlYW4gYmVmb3JlIHN0YXJ0aW5nLicpO1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koJ2RlZmluZS12cGMnLCB7IG1vZEVudjogeyBFTkFCTEVfVlBDX1RFU1RJTkc6ICdERUZJTkUnIH0gfSk7XG5cbiAgZml4dHVyZS5sb2coJ1NldHRpbmcgdXA6IGNyZWF0aW5nIGEgVlBDIHdpdGgga25vd24gdGFncycpO1xuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZGVmaW5lLXZwYycsIHsgbW9kRW52OiB7IEVOQUJMRV9WUENfVEVTVElORzogJ0RFRklORScgfSB9KTtcbiAgZml4dHVyZS5sb2coJ1NldHVwIGNvbXBsZXRlIScpO1xuXG4gIGZpeHR1cmUubG9nKCdWZXJpZnlpbmcgd2UgY2FuIG5vdyBpbXBvcnQgdGhhdCBWUEMnKTtcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ2ltcG9ydC12cGMnLCB7IG1vZEVudjogeyBFTkFCTEVfVlBDX1RFU1RJTkc6ICdJTVBPUlQnIH0gfSk7XG59KSk7XG5cbmludGVnVGVzdCgnVHdvIHdheXMgb2Ygc2hvaW5nIHRoZSB2ZXJzaW9uJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHZlcnNpb24xID0gYXdhaXQgZml4dHVyZS5jZGsoWyd2ZXJzaW9uJ10sIHsgdmVyYm9zZTogZmFsc2UgfSk7XG4gIGNvbnN0IHZlcnNpb24yID0gYXdhaXQgZml4dHVyZS5jZGsoWyctLXZlcnNpb24nXSwgeyB2ZXJib3NlOiBmYWxzZSB9KTtcblxuICBleHBlY3QodmVyc2lvbjEpLnRvRXF1YWwodmVyc2lvbjIpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ1Rlcm1pbmF0aW9uIHByb3RlY3Rpb24nLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgc3RhY2tOYW1lID0gJ3Rlcm1pbmF0aW9uLXByb3RlY3Rpb24nO1xuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveShzdGFja05hbWUpO1xuXG4gIC8vIFRyeSBhIGRlc3Ryb3kgdGhhdCBzaG91bGQgZmFpbFxuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGtEZXN0cm95KHN0YWNrTmFtZSkpLnJlamVjdHMudG9UaHJvdygnZXhpdGVkIHdpdGggZXJyb3InKTtcblxuICAvLyBDYW4gdXBkYXRlIHRlcm1pbmF0aW9uIHByb3RlY3Rpb24gZXZlbiB0aG91Z2ggdGhlIGNoYW5nZSBzZXQgZG9lc24ndCBjb250YWluIGNoYW5nZXNcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lLCB7IG1vZEVudjogeyBURVJNSU5BVElPTl9QUk9URUNUSU9OOiAnRkFMU0UnIH0gfSk7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVzdHJveShzdGFja05hbWUpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NkayBzeW50aCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGsoWydzeW50aCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldLCB7IHZlcmJvc2U6IGZhbHNlIH0pKS5yZXNvbHZlcy50b0VxdWFsKFxuICAgIGBSZXNvdXJjZXM6XG4gIHRvcGljNjk4MzE0OTE6XG4gICAgVHlwZTogQVdTOjpTTlM6OlRvcGljXG4gICAgTWV0YWRhdGE6XG4gICAgICBhd3M6Y2RrOnBhdGg6ICR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMS90b3BpYy9SZXNvdXJjZWApO1xuXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ3N5bnRoJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0sIHsgdmVyYm9zZTogZmFsc2UgfSkpLnJlc29sdmVzLnRvRXF1YWwoXG4gICAgYFJlc291cmNlczpcbiAgdG9waWMxNTJEODRBMzc6XG4gICAgVHlwZTogQVdTOjpTTlM6OlRvcGljXG4gICAgTWV0YWRhdGE6XG4gICAgICBhd3M6Y2RrOnBhdGg6ICR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMi90b3BpYzEvUmVzb3VyY2VcbiAgdG9waWMyQTRGQjU0N0Y6XG4gICAgVHlwZTogQVdTOjpTTlM6OlRvcGljXG4gICAgTWV0YWRhdGE6XG4gICAgICBhd3M6Y2RrOnBhdGg6ICR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMi90b3BpYzIvUmVzb3VyY2VgKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzc20gcGFyYW1ldGVyIHByb3ZpZGVyIGVycm9yJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ3N5bnRoJyxcbiAgICBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ21pc3Npbmctc3NtLXBhcmFtZXRlcicpLFxuICAgICctYycsICd0ZXN0OnNzbS1wYXJhbWV0ZXItbmFtZT0vZG9lcy9ub3QvZXhpc3QnXSwge1xuICAgIGFsbG93RXJyRXhpdDogdHJ1ZSxcbiAgfSkpLnJlc29sdmVzLnRvQ29udGFpbignU1NNIHBhcmFtZXRlciBub3QgYXZhaWxhYmxlIGluIGFjY291bnQnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdhdXRvbWF0aWMgb3JkZXJpbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gRGVwbG95IHRoZSBjb25zdW1pbmcgc3RhY2sgd2hpY2ggd2lsbCBpbmNsdWRlIHRoZSBwcm9kdWNpbmcgc3RhY2tcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ29yZGVyLWNvbnN1bWluZycpO1xuXG4gIC8vIERlc3Ryb3kgdGhlIHByb3ZpZGluZyBzdGFjayB3aGljaCB3aWxsIGluY2x1ZGUgdGhlIGNvbnN1bWluZyBzdGFja1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koJ29yZGVyLXByb3ZpZGluZycpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NvbnRleHQgc2V0dGluZycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBmcy53cml0ZUZpbGUocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpLCBKU09OLnN0cmluZ2lmeSh7XG4gICAgY29udGV4dGtleTogJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnLFxuICB9KSk7XG4gIHRyeSB7XG4gICAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrKFsnY29udGV4dCddKSkucmVzb2x2ZXMudG9Db250YWluKCd0aGlzIGlzIHRoZSBjb250ZXh0IHZhbHVlJyk7XG5cbiAgICAvLyBUZXN0IHRoYXQgZGVsZXRpbmcgdGhlIGNvbnRleHRrZXkgd29ya3NcbiAgICBhd2FpdCBmaXh0dXJlLmNkayhbJ2NvbnRleHQnLCAnLS1yZXNldCcsICdjb250ZXh0a2V5J10pO1xuICAgIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2NvbnRleHQnXSkpLnJlc29sdmVzLm5vdC50b0NvbnRhaW4oJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnKTtcblxuICAgIC8vIFRlc3QgdGhhdCBmb3JjZWQgZGVsZXRlIG9mIHRoZSBjb250ZXh0IGtleSBkb2VzIG5vdCB0aHJvd1xuICAgIGF3YWl0IGZpeHR1cmUuY2RrKFsnY29udGV4dCcsICctZicsICctLXJlc2V0JywgJ2NvbnRleHRrZXknXSk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmcy51bmxpbmsocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCd0ZXN0LTInLCB7IGNhcHR1cmVTdGRlcnI6IGZhbHNlIH0pO1xuXG4gIC8vIHZlcmlmeSB0aGUgbnVtYmVyIG9mIHJlc291cmNlcyBpbiB0aGUgc3RhY2tcbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja1Jlc291cmNlcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrUmVzb3VyY2VzPy5sZW5ndGgpLnRvRXF1YWwoMik7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IGFsbCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBhcm5zID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtKicsIHsgY2FwdHVyZVN0ZGVycjogZmFsc2UgfSk7XG5cbiAgLy8gdmVyaWZ5IHRoYXQgd2Ugb25seSBkZXBsb3llZCBhIHNpbmdsZSBzdGFjayAodGhlcmUncyBhIHNpbmdsZSBBUk4gaW4gdGhlIG91dHB1dClcbiAgZXhwZWN0KGFybnMuc3BsaXQoJ1xcbicpLmxlbmd0aCkudG9FcXVhbCgyKTtcbn0pKTtcblxuaW50ZWdUZXN0KCduZXN0ZWQgc3RhY2sgd2l0aCBwYXJhbWV0ZXJzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIC8vIFNUQUNLX05BTUVfUFJFRklYIGlzIHVzZWQgaW4gTXlUb3BpY1BhcmFtIHRvIGFsbG93IG11bHRpcGxlIGluc3RhbmNlc1xuICAvLyBvZiB0aGlzIHRlc3QgdG8gcnVuIGluIHBhcmFsbGVsLCBvdGhld2lzZSB0aGV5IHdpbGwgYXR0ZW1wdCB0byBjcmVhdGUgdGhlIHNhbWUgU05TIHRvcGljLlxuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCd3aXRoLW5lc3RlZC1zdGFjay11c2luZy1wYXJhbWV0ZXJzJywge1xuICAgIG9wdGlvbnM6IFsnLS1wYXJhbWV0ZXJzJywgYE15VG9waWNQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fVRoZXJlSXNOb1Nwb29uYF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIC8vIHZlcmlmeSB0aGF0IHdlIG9ubHkgZGVwbG95ZWQgYSBzaW5nbGUgc3RhY2sgKHRoZXJlJ3MgYSBzaW5nbGUgQVJOIGluIHRoZSBvdXRwdXQpXG4gIGV4cGVjdChzdGFja0Fybi5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDEpO1xuXG4gIC8vIHZlcmlmeSB0aGUgbnVtYmVyIG9mIHJlc291cmNlcyBpbiB0aGUgc3RhY2tcbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja1Jlc291cmNlcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrUmVzb3VyY2VzPy5sZW5ndGgpLnRvRXF1YWwoMSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGhvdXQgZXhlY3V0ZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCd0ZXN0LTInLCB7XG4gICAgb3B0aW9uczogWyctLW5vLWV4ZWN1dGUnXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG4gIC8vIHZlcmlmeSB0aGF0IHdlIG9ubHkgZGVwbG95ZWQgYSBzaW5nbGUgc3RhY2sgKHRoZXJlJ3MgYSBzaW5nbGUgQVJOIGluIHRoZSBvdXRwdXQpXG4gIGV4cGVjdChzdGFja0Fybi5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDEpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuXG4gIGV4cGVjdChyZXNwb25zZS5TdGFja3M/LlswXS5TdGFja1N0YXR1cykudG9FcXVhbCgnUkVWSUVXX0lOX1BST0dSRVNTJyk7XG59KSk7XG5cbmludGVnVGVzdCgnc2VjdXJpdHkgcmVsYXRlZCBjaGFuZ2VzIHdpdGhvdXQgYSBDTEkgYXJlIGV4cGVjdGVkIHRvIGZhaWwnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gcmVkaXJlY3QgL2Rldi9udWxsIHRvIHN0ZGluLCB3aGljaCBtZWFucyB0aGVyZSB3aWxsIG5vdCBiZSB0dHkgYXR0YWNoZWRcbiAgLy8gc2luY2UgdGhpcyBzdGFjayBpbmNsdWRlcyBzZWN1cml0eS1yZWxhdGVkIGNoYW5nZXMsIHRoZSBkZXBsb3ltZW50IHNob3VsZFxuICAvLyBpbW1lZGlhdGVseSBmYWlsIGJlY2F1c2Ugd2UgY2FuJ3QgY29uZmlybSB0aGUgY2hhbmdlc1xuICBjb25zdCBzdGFja05hbWUgPSAnaWFtLXRlc3QnO1xuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lLCB7XG4gICAgb3B0aW9uczogWyc8JywgJy9kZXYvbnVsbCddLCAvLyBINHgsIHRoaXMgb25seSB3b3JrcyBiZWNhdXNlIEkgaGFwcGVuIHRvIGtub3cgd2UgcGFzcyBzaGVsbDogdHJ1ZS5cbiAgICBuZXZlclJlcXVpcmVBcHByb3ZhbDogZmFsc2UsXG4gIH0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG5cbiAgLy8gRW5zdXJlIHN0YWNrIHdhcyBub3QgZGVwbG95ZWRcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFja05hbWUpLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdkb2VzIG5vdCBleGlzdCcpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aWxkY2FyZCB3aXRoIG91dHB1dHMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgb3V0cHV0c0ZpbGUgPSBwYXRoLmpvaW4oZml4dHVyZS5pbnRlZ1Rlc3REaXIsICdvdXRwdXRzJywgJ291dHB1dHMuanNvbicpO1xuICBhd2FpdCBmcy5ta2RpcihwYXRoLmRpcm5hbWUob3V0cHV0c0ZpbGUpLCB7IHJlY3Vyc2l2ZTogdHJ1ZSB9KTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveShbJ291dHB1dHMtdGVzdC0qJ10sIHtcbiAgICBvcHRpb25zOiBbJy0tb3V0cHV0cy1maWxlJywgb3V0cHV0c0ZpbGVdLFxuICB9KTtcblxuICBjb25zdCBvdXRwdXRzID0gSlNPTi5wYXJzZSgoYXdhaXQgZnMucmVhZEZpbGUob3V0cHV0c0ZpbGUsIHsgZW5jb2Rpbmc6ICd1dGYtOCcgfSkpLnRvU3RyaW5nKCkpO1xuICBleHBlY3Qob3V0cHV0cykudG9FcXVhbCh7XG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMWBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMU15VG9waWNgLFxuICAgIH0sXG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMmBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMk15T3RoZXJUb3BpY2AsXG4gICAgfSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWJhemluZ2FgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuXG4gIGV4cGVjdChyZXNwb25zZS5TdGFja3M/LlswXS5QYXJhbWV0ZXJzKS50b0VxdWFsKFtcbiAgICB7XG4gICAgICBQYXJhbWV0ZXJLZXk6ICdUb3BpY05hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YmF6aW5nYWAsXG4gICAgfSxcbiAgXSk7XG59KSk7XG5cbmludGVnVGVzdCgndXBkYXRlIHRvIHN0YWNrIGluIFJPTExCQUNLX0NPTVBMRVRFIHN0YXRlIHdpbGwgZGVsZXRlIHN0YWNrIGFuZCBjcmVhdGUgYSBuZXcgb25lJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIC8vIEdJVkVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogZml4dHVyZS5mdWxsU3RhY2tOYW1lKCdwYXJhbS10ZXN0LTEnKSxcbiAgfSk7XG5cbiAgY29uc3Qgc3RhY2tBcm4gPSByZXNwb25zZS5TdGFja3M/LlswXS5TdGFja0lkO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1JPTExCQUNLX0NPTVBMRVRFJyk7XG5cbiAgLy8gV0hFTlxuICBjb25zdCBuZXdTdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IG5ld1N0YWNrUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBuZXdTdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QgKHN0YWNrQXJuKS5ub3QudG9FcXVhbChuZXdTdGFja0Fybik7IC8vIG5ldyBzdGFjayB3YXMgY3JlYXRlZFxuICBleHBlY3QobmV3U3RhY2tSZXNwb25zZS5TdGFja3M/LlswXS5TdGFja1N0YXR1cykudG9FcXVhbCgnQ1JFQVRFX0NPTVBMRVRFJyk7XG4gIGV4cGVjdChuZXdTdGFja1Jlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ1RvcGljTmFtZVBhcmFtJyxcbiAgICAgIFBhcmFtZXRlclZhbHVlOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1hbGxnb29kYCxcbiAgICB9LFxuICBdKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzdGFjayBpbiBVUERBVEVfUk9MTEJBQ0tfQ09NUExFVEUgc3RhdGUgY2FuIGJlIHVwZGF0ZWQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1uaWNlYCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KTtcblxuICBsZXQgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlN0YWNrU3RhdHVzKS50b0VxdWFsKCdDUkVBVEVfQ09NUExFVEUnKTtcblxuICAvLyBiYWQgcGFyYW1ldGVyIG5hbWUgd2l0aCBAIHdpbGwgcHV0IHN0YWNrIGludG8gVVBEQVRFX1JPTExCQUNLX0NPTVBMRVRFXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpOztcblxuICByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcblxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9ST0xMQkFDS19DT01QTEVURScpO1xuXG4gIC8vIFdIRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYFRvcGljTmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YWxsZ29vZGAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9DT01QTEVURScpO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uUGFyYW1ldGVycykudG9FcXVhbChbXG4gICAge1xuICAgICAgUGFyYW1ldGVyS2V5OiAnVG9waWNOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIHdpbGRjYXJkIGFuZCBwYXJhbWV0ZXJzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LSonLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTE6VG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tcGFyYW0tdGVzdC0yOk90aGVyVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1UaGF0c015U3BvdGAsXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXBhcmFtLXRlc3QtMzpEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9SGV5VGhlcmVgLFxuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTM6T3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9QW5vdGhlck9uZWAsXG4gICAgXSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycyBtdWx0aScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBwYXJhbVZhbDEgPSBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYDtcbiAgY29uc3QgcGFyYW1WYWwyID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9PWphZ3NoZW1hc2hgO1xuXG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMycsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYERpc3BsYXlOYW1lUGFyYW09JHtwYXJhbVZhbDF9YCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgT3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7cGFyYW1WYWwyfWAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ0Rpc3BsYXlOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IHBhcmFtVmFsMSxcbiAgICB9LFxuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ090aGVyRGlzcGxheU5hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogcGFyYW1WYWwyLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIG5vdGlmaWNhdGlvbiBBUk4nLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgdG9waWNOYW1lID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtdG9waWNgO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3Muc25zKCdjcmVhdGVUb3BpYycsIHsgTmFtZTogdG9waWNOYW1lIH0pO1xuICBjb25zdCB0b3BpY0FybiA9IHJlc3BvbnNlLlRvcGljQXJuITtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJywge1xuICAgICAgb3B0aW9uczogWyctLW5vdGlmaWNhdGlvbi1hcm5zJywgdG9waWNBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gdmVyaWZ5IHRoYXQgdGhlIHN0YWNrIHdlIGRlcGxveWVkIGhhcyBvdXIgbm90aWZpY2F0aW9uIEFSTlxuICAgIGNvbnN0IGRlc2NyaWJlUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyksXG4gICAgfSk7XG4gICAgZXhwZWN0KGRlc2NyaWJlUmVzcG9uc2UuU3RhY2tzPy5bMF0uTm90aWZpY2F0aW9uQVJOcykudG9FcXVhbChbdG9waWNBcm5dKTtcbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5zbnMoJ2RlbGV0ZVRvcGljJywge1xuICAgICAgVG9waWNBcm46IHRvcGljQXJuLFxuICAgIH0pO1xuICB9XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcm9sZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCByb2xlTmFtZSA9IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS10ZXN0LXJvbGVgO1xuXG4gIGF3YWl0IGRlbGV0ZVJvbGUoKTtcblxuICBjb25zdCBjcmVhdGVSZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnY3JlYXRlUm9sZScsIHtcbiAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgQXNzdW1lUm9sZVBvbGljeURvY3VtZW50OiBKU09OLnN0cmluZ2lmeSh7XG4gICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICBTdGF0ZW1lbnQ6IFt7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IFNlcnZpY2U6ICdjbG91ZGZvcm1hdGlvbi5hbWF6b25hd3MuY29tJyB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9LCB7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IEFXUzogKGF3YWl0IGZpeHR1cmUuYXdzLnN0cygnZ2V0Q2FsbGVySWRlbnRpdHknLCB7fSkpLkFybiB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9XSxcbiAgICB9KSxcbiAgfSk7XG4gIGNvbnN0IHJvbGVBcm4gPSBjcmVhdGVSZXNwb25zZS5Sb2xlLkFybjtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ3B1dFJvbGVQb2xpY3knLCB7XG4gICAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgICBQb2xpY3lOYW1lOiAnRGVmYXVsdFBvbGljeScsXG4gICAgICBQb2xpY3lEb2N1bWVudDogSlNPTi5zdHJpbmdpZnkoe1xuICAgICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICAgIFN0YXRlbWVudDogW3tcbiAgICAgICAgICBBY3Rpb246ICcqJyxcbiAgICAgICAgICBSZXNvdXJjZTogJyonLFxuICAgICAgICAgIEVmZmVjdDogJ0FsbG93JyxcbiAgICAgICAgfV0sXG4gICAgICB9KSxcbiAgICB9KTtcblxuICAgIGF3YWl0IHJldHJ5KGZpeHR1cmUub3V0cHV0LCAnVHJ5aW5nIHRvIGFzc3VtZSBmcmVzaCByb2xlJywgcmV0cnkuZm9yU2Vjb25kcygzMDApLCBhc3luYyAoKSA9PiB7XG4gICAgICBhd2FpdCBmaXh0dXJlLmF3cy5zdHMoJ2Fzc3VtZVJvbGUnLCB7XG4gICAgICAgIFJvbGVBcm46IHJvbGVBcm4sXG4gICAgICAgIFJvbGVTZXNzaW9uTmFtZTogJ3Rlc3RpbmcnLFxuICAgICAgfSk7XG4gICAgfSk7XG5cbiAgICAvLyBJbiBwcmluY2lwbGUsIHRoZSByb2xlIGhhcyByZXBsaWNhdGVkIGZyb20gJ3VzLWVhc3QtMScgdG8gd2hlcmV2ZXIgd2UncmUgdGVzdGluZy5cbiAgICAvLyBHaXZlIGl0IGEgbGl0dGxlIG1vcmUgc2xlZXAgdG8gbWFrZSBzdXJlIENsb3VkRm9ybWF0aW9uIGlzIG5vdCBoaXR0aW5nIGEgYm94XG4gICAgLy8gdGhhdCBkb2Vzbid0IGhhdmUgaXQgeWV0LlxuICAgIGF3YWl0IHNsZWVwKDUwMDApO1xuXG4gICAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMicsIHtcbiAgICAgIG9wdGlvbnM6IFsnLS1yb2xlLWFybicsIHJvbGVBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gSW1tZWRpYXRlbHkgZGVsZXRlIHRoZSBzdGFjayBhZ2FpbiBiZWZvcmUgd2UgZGVsZXRlIHRoZSByb2xlLlxuICAgIC8vXG4gICAgLy8gU2luY2Ugcm9sZXMgYXJlIHN0aWNreSwgaWYgd2UgZGVsZXRlIHRoZSByb2xlIGJlZm9yZSB0aGUgc3RhY2ssIHN1YnNlcXVlbnQgRGVsZXRlU3RhY2tcbiAgICAvLyBvcGVyYXRpb25zIHdpbGwgZmFpbCB3aGVuIENsb3VkRm9ybWF0aW9uIHRyaWVzIHRvIGFzc3VtZSB0aGUgcm9sZSB0aGF0J3MgYWxyZWFkeSBnb25lLlxuICAgIGF3YWl0IGZpeHR1cmUuY2RrRGVzdHJveSgndGVzdC0yJyk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBkZWxldGVSb2xlKCk7XG4gIH1cblxuICBhc3luYyBmdW5jdGlvbiBkZWxldGVSb2xlKCkge1xuICAgIHRyeSB7XG4gICAgICBmb3IgKGNvbnN0IHBvbGljeU5hbWUgb2YgKGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnbGlzdFJvbGVQb2xpY2llcycsIHsgUm9sZU5hbWU6IHJvbGVOYW1lIH0pKS5Qb2xpY3lOYW1lcykge1xuICAgICAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ2RlbGV0ZVJvbGVQb2xpY3knLCB7XG4gICAgICAgICAgUm9sZU5hbWU6IHJvbGVOYW1lLFxuICAgICAgICAgIFBvbGljeU5hbWU6IHBvbGljeU5hbWUsXG4gICAgICAgIH0pO1xuICAgICAgfVxuICAgICAgYXdhaXQgZml4dHVyZS5hd3MuaWFtKCdkZWxldGVSb2xlJywgeyBSb2xlTmFtZTogcm9sZU5hbWUgfSk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgaWYgKGUubWVzc2FnZS5pbmRleE9mKCdjYW5ub3QgYmUgZm91bmQnKSA+IC0xKSB7IHJldHVybjsgfVxuICAgICAgdGhyb3cgZTtcbiAgICB9XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignQVdTOjpTTlM6OlRvcGljJyk7XG5cbiAgLy8gV2UgY2FuIG1ha2UgaXQgZmFpbCBieSBwYXNzaW5nIC0tZmFpbFxuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGsoWydkaWZmJywgJy0tZmFpbCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKSlcbiAgICAucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NkayBkaWZmIC0tZmFpbCBvbiBtdWx0aXBsZSBzdGFja3MgZXhpdHMgd2l0aCBlcnJvciBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3QgZGlmZjEgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMScpXSk7XG4gIGV4cGVjdChkaWZmMSkudG9Db250YWluKCdBV1M6OlNOUzo6VG9waWMnKTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJyk7XG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignVGhlcmUgd2VyZSBubyBkaWZmZXJlbmNlcycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnY2RrIGRpZmYgLS1mYWlsIHdpdGggbXVsdGlwbGUgc3RhY2sgZXhpdHMgd2l0aCBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMScpO1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ1RoZXJlIHdlcmUgbm8gZGlmZmVyZW5jZXMnKTtcblxuICBjb25zdCBkaWZmMiA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyldKTtcbiAgZXhwZWN0KGRpZmYyKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHN0YWNrIHdpdGggZG9ja2VyIGFzc2V0Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdkb2NrZXInKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgYW5kIHRlc3Qgc3RhY2sgd2l0aCBsYW1iZGEgYXNzZXQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnbGFtYmRhJywgeyBjYXB0dXJlU3RkZXJyOiBmYWxzZSB9KTtcblxuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcbiAgY29uc3QgbGFtYmRhQXJuID0gcmVzcG9uc2UuU3RhY2tzPy5bMF0uT3V0cHV0cz8uWzBdLk91dHB1dFZhbHVlO1xuICBpZiAobGFtYmRhQXJuID09PSB1bmRlZmluZWQpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ1N0YWNrIGRpZCBub3QgaGF2ZSBleHBlY3RlZCBMYW1iZGEgQVJOIG91dHB1dCcpO1xuICB9XG5cbiAgY29uc3Qgb3V0cHV0ID0gYXdhaXQgZml4dHVyZS5hd3MubGFtYmRhKCdpbnZva2UnLCB7XG4gICAgRnVuY3Rpb25OYW1lOiBsYW1iZGFBcm4sXG4gIH0pO1xuXG4gIGV4cGVjdChKU09OLnN0cmluZ2lmeShvdXRwdXQuUGF5bG9hZCkpLnRvQ29udGFpbignZGVhciBhc3NldCcpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NkayBscycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBsaXN0aW5nID0gYXdhaXQgZml4dHVyZS5jZGsoWydscyddLCB7IGNhcHR1cmVTdGRlcnI6IGZhbHNlIH0pO1xuXG4gIGNvbnN0IGV4cGVjdGVkU3RhY2tzID0gW1xuICAgICdjb25kaXRpb25hbC1yZXNvdXJjZScsXG4gICAgJ2RvY2tlcicsXG4gICAgJ2RvY2tlci13aXRoLWN1c3RvbS1maWxlJyxcbiAgICAnZmFpbGVkJyxcbiAgICAnaWFtLXRlc3QnLFxuICAgICdsYW1iZGEnLFxuICAgICdtaXNzaW5nLXNzbS1wYXJhbWV0ZXInLFxuICAgICdvcmRlci1wcm92aWRpbmcnLFxuICAgICdvdXRwdXRzLXRlc3QtMScsXG4gICAgJ291dHB1dHMtdGVzdC0yJyxcbiAgICAncGFyYW0tdGVzdC0xJyxcbiAgICAncGFyYW0tdGVzdC0yJyxcbiAgICAncGFyYW0tdGVzdC0zJyxcbiAgICAndGVybWluYXRpb24tcHJvdGVjdGlvbicsXG4gICAgJ3Rlc3QtMScsXG4gICAgJ3Rlc3QtMicsXG4gICAgJ3dpdGgtbmVzdGVkLXN0YWNrJyxcbiAgICAnd2l0aC1uZXN0ZWQtc3RhY2stdXNpbmctcGFyYW1ldGVycycsXG4gICAgJ29yZGVyLWNvbnN1bWluZycsXG4gIF07XG5cbiAgZm9yIChjb25zdCBzdGFjayBvZiBleHBlY3RlZFN0YWNrcykge1xuICAgIGV4cGVjdChsaXN0aW5nKS50b0NvbnRhaW4oZml4dHVyZS5mdWxsU3RhY2tOYW1lKHN0YWNrKSk7XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgc3RhY2sgd2l0aG91dCByZXNvdXJjZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGhvdXQgcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScsIHsgbW9kRW52OiB7IE5PX1JFU09VUkNFOiAnVFJVRScgfSB9KTtcblxuICAvLyBUaGlzIHNob3VsZCBoYXZlIHN1Y2NlZWRlZCBidXQgbm90IGRlcGxveWVkIHRoZSBzdGFjay5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGggcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScpO1xuXG4gIC8vIFRoZW4gYWdhaW4gV0lUSE9VVCByZXNvdXJjZXMgKHRoaXMgc2hvdWxkIGRlc3Ryb3kgdGhlIHN0YWNrKVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnY29uZGl0aW9uYWwtcmVzb3VyY2UnLCB7IG1vZEVudjogeyBOT19SRVNPVVJDRTogJ1RSVUUnIH0gfSk7XG5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdJQU0gZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBvdXRwdXQgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2lhbS10ZXN0JyldKTtcblxuICAvLyBSb3VnaGx5IGNoZWNrIGZvciBhIHRhYmxlIGxpa2UgdGhpczpcbiAgLy9cbiAgLy8g4pSM4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSALeKUgOKUgOKUrOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUkFxuICAvLyDilIIgICDilIIgUmVzb3VyY2UgICAgICAgIOKUgiBFZmZlY3Qg4pSCIEFjdGlvbiAgICAgICAgIOKUgiBQcmluY2lwYWwgICAgICAgICAgICAgICAgICAgICDilIIgQ29uZGl0aW9uIOKUglxuICAvLyDilJzilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilKRcbiAgLy8g4pSCICsg4pSCICR7U29tZVJvbGUuQXJufSDilIIgQWxsb3cgIOKUgiBzdHM6QXNzdW1lUm9sZSDilIIgU2VydmljZTplYzIuYW1hem9uYXdzLmNvbSAgICAg4pSCICAgICAgICAgICDilIJcbiAgLy8g4pSU4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSYXG5cbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCcke1NvbWVSb2xlLkFybn0nKTtcbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCdzdHM6QXNzdW1lUm9sZScpO1xuICBleHBlY3Qob3V0cHV0KS50b0NvbnRhaW4oJ2VjMi5hbWF6b25hd3MuY29tJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZmFzdCBkZXBsb3knLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gd2UgYXJlIHVzaW5nIGEgc3RhY2sgd2l0aCBhIG5lc3RlZCBzdGFjayBiZWNhdXNlIENGTiB3aWxsIGFsd2F5cyBhdHRlbXB0IHRvXG4gIC8vIHVwZGF0ZSBhIG5lc3RlZCBzdGFjaywgd2hpY2ggd2lsbCBhbGxvdyB1cyB0byB2ZXJpZnkgdGhhdCB1cGRhdGVzIGFyZSBhY3R1YWxseVxuICAvLyBza2lwcGVkIHVubGVzcyAtLWZvcmNlIGlzIHNwZWNpZmllZC5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IGNhcHR1cmVTdGRlcnI6IGZhbHNlIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQxID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzYW1lIHN0YWNrIGFnYWluLCB0aGVyZSBzaG91bGQgYmUgbm8gbmV3IGNoYW5nZSBzZXQgY3JlYXRlZFxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snKTtcbiAgY29uc3QgY2hhbmdlU2V0MiA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCkudG9FcXVhbChjaGFuZ2VTZXQxLkNoYW5nZVNldElkKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIGFnYWluIHdpdGggLS1mb3JjZSwgbm93IHdlIHNob3VsZCBjcmVhdGUgYSBjaGFuZ2VzZXRcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3dpdGgtbmVzdGVkLXN0YWNrJywgeyBvcHRpb25zOiBbJy0tZm9yY2UnXSB9KTtcbiAgY29uc3QgY2hhbmdlU2V0MyA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0My5DaGFuZ2VTZXRJZCkubm90LnRvRXF1YWwoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzdGFjayBhZ2FpbiB3aXRoIHRhZ3MsIGV4cGVjdGVkIHRvIGNyZWF0ZSBhIG5ldyBjaGFuZ2VzZXRcbiAgLy8gZXZlbiB0aG91Z2ggdGhlIHJlc291cmNlcyBkaWRuJ3QgY2hhbmdlLlxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IG9wdGlvbnM6IFsnLS10YWdzJywgJ2tleT12YWx1ZSddIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQ0ID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG4gIGV4cGVjdChjaGFuZ2VTZXQ0LkNoYW5nZVNldElkKS5ub3QudG9FcXVhbChjaGFuZ2VTZXQzLkNoYW5nZVNldElkKTtcblxuICBhc3luYyBmdW5jdGlvbiBnZXRMYXRlc3RDaGFuZ2VTZXQoKSB7XG4gICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7IFN0YWNrTmFtZTogc3RhY2tBcm4gfSk7XG4gICAgaWYgKCFyZXNwb25zZS5TdGFja3M/LlswXSkgeyB0aHJvdyBuZXcgRXJyb3IoJ0RpZCBub3QgZ2V0IGEgQ2hhbmdlU2V0IGF0IGFsbCcpOyB9XG4gICAgZml4dHVyZS5sb2coYEZvdW5kIENoYW5nZSBTZXQgJHtyZXNwb25zZS5TdGFja3M/LlswXS5DaGFuZ2VTZXRJZH1gKTtcbiAgICByZXR1cm4gcmVzcG9uc2UuU3RhY2tzPy5bMF07XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdmYWlsZWQgZGVwbG95IGRvZXMgbm90IGhhbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gdGhpcyB3aWxsIGhhbmcgaWYgd2UgaW50cm9kdWNlIGh0dHBzOi8vZ2l0aHViLmNvbS9hd3MvYXdzLWNkay9pc3N1ZXMvNjQwMyBhZ2Fpbi5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrRGVwbG95KCdmYWlsZWQnKSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NhbiBzdGlsbCBsb2FkIG9sZCBhc3NlbWJsaWVzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGN4QXNtRGlyID0gcGF0aC5qb2luKG9zLnRtcGRpcigpLCAnY2RrLWludGVnLWN4Jyk7XG5cbiAgY29uc3QgdGVzdEFzc2VtYmxpZXNEaXJlY3RvcnkgPSBwYXRoLmpvaW4oX19kaXJuYW1lLCAnY2xvdWQtYXNzZW1ibGllcycpO1xuICBmb3IgKGNvbnN0IGFzbWRpciBvZiBhd2FpdCBsaXN0Q2hpbGREaXJzKHRlc3RBc3NlbWJsaWVzRGlyZWN0b3J5KSkge1xuICAgIGZpeHR1cmUubG9nKGBBU1NFTUJMWSAke2FzbWRpcn1gKTtcbiAgICBhd2FpdCBjbG9uZURpcmVjdG9yeShhc21kaXIsIGN4QXNtRGlyKTtcblxuICAgIC8vIFNvbWUgZmlsZXMgaW4gdGhlIGFzbSBkaXJlY3RvcnkgdGhhdCBoYXZlIGEgLmpzIGV4dGVuc2lvbiBhcmVcbiAgICAvLyBhY3R1YWxseSB0cmVhdGVkIGFzIHRlbXBsYXRlcy4gRXZhbHVhdGUgdGhlbSB1c2luZyBOb2RlSlMuXG4gICAgY29uc3QgdGVtcGxhdGVzID0gYXdhaXQgbGlzdENoaWxkcmVuKGN4QXNtRGlyLCBmdWxsUGF0aCA9PiBQcm9taXNlLnJlc29sdmUoZnVsbFBhdGguZW5kc1dpdGgoJy5qcycpKSk7XG4gICAgZm9yIChjb25zdCB0ZW1wbGF0ZSBvZiB0ZW1wbGF0ZXMpIHtcbiAgICAgIGNvbnN0IHRhcmdldE5hbWUgPSB0ZW1wbGF0ZS5yZXBsYWNlKC8uanMkLywgJycpO1xuICAgICAgYXdhaXQgc2hlbGwoW3Byb2Nlc3MuZXhlY1BhdGgsIHRlbXBsYXRlLCAnPicsIHRhcmdldE5hbWVdLCB7XG4gICAgICAgIGN3ZDogY3hBc21EaXIsXG4gICAgICAgIG91dHB1dDogZml4dHVyZS5vdXRwdXQsXG4gICAgICAgIG1vZEVudjoge1xuICAgICAgICAgIFRFU1RfQUNDT1VOVDogYXdhaXQgZml4dHVyZS5hd3MuYWNjb3VudCgpLFxuICAgICAgICAgIFRFU1RfUkVHSU9OOiBmaXh0dXJlLmF3cy5yZWdpb24sXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9XG5cbiAgICAvLyBVc2UgdGhpcyBkaXJlY3RvcnkgYXMgYSBDbG91ZCBBc3NlbWJseVxuICAgIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuY2RrKFtcbiAgICAgICctLWFwcCcsIGN4QXNtRGlyLFxuICAgICAgJy12JyxcbiAgICAgICdzeW50aCcsXG4gICAgXSk7XG5cbiAgICAvLyBBc3NlcnQgdGhhdCB0aGVyZSB3YXMgbm8gcHJvdmlkZXJFcnJvciBpbiBDREsncyBzdGRlcnJcbiAgICAvLyBCZWNhdXNlIHdlIHJlbHkgb24gdGhlIGFwcC9mcmFtZXdvcmsgdG8gYWN0dWFsbHkgZXJyb3IgaW4gY2FzZSB0aGVcbiAgICAvLyBwcm92aWRlciBmYWlscywgd2UgaW5zcGVjdCB0aGUgbG9ncyBoZXJlLlxuICAgIGV4cGVjdChvdXRwdXQpLm5vdC50b0NvbnRhaW4oJyRwcm92aWRlckVycm9yJyk7XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdnZW5lcmF0aW5nIGFuZCBsb2FkaW5nIGFzc2VtYmx5Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGFzbU91dHB1dERpciA9IGAke2ZpeHR1cmUuaW50ZWdUZXN0RGlyfS1jZGstaW50ZWctYXNtYDtcbiAgYXdhaXQgZml4dHVyZS5zaGVsbChbJ3JtJywgJy1yZicsIGFzbU91dHB1dERpcl0pO1xuXG4gIC8vIFN5bnRoZXNpemUgYSBDbG91ZCBBc3NlbWJseSB0b3RoZSBkZWZhdWx0IGRpcmVjdG9yeSAoY2RrLm91dCkgYW5kIGEgc3BlY2lmaWMgZGlyZWN0b3J5LlxuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJ10pO1xuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJywgJy0tb3V0cHV0JywgYXNtT3V0cHV0RGlyXSk7XG5cbiAgLy8gY2RrLm91dCBpbiB0aGUgY3VycmVudCBkaXJlY3RvcnkgYW5kIHRoZSBpbmRpY2F0ZWQgLS1vdXRwdXQgc2hvdWxkIGJlIHRoZSBzYW1lXG4gIGF3YWl0IGZpeHR1cmUuc2hlbGwoWydkaWZmJywgJ2Nkay5vdXQnLCBhc21PdXRwdXREaXJdKTtcblxuICAvLyBDaGVjayB0aGF0IHdlIGNhbiAnbHMnIHRoZSBzeW50aGVzaXplZCBhc20uXG4gIC8vIENoYW5nZSB0byBzb21lIHJhbmRvbSBkaXJlY3RvcnkgdG8gbWFrZSBzdXJlIHdlJ3JlIG5vdCBhY2NpZGVudGFsbHkgbG9hZGluZyBjZGsuanNvblxuICBjb25zdCBsaXN0ID0gYXdhaXQgZml4dHVyZS5jZGsoWyctLWFwcCcsIGFzbU91dHB1dERpciwgJ2xzJ10sIHsgY3dkOiBvcy50bXBkaXIoKSB9KTtcbiAgLy8gU2FtZSBzdGFja3Mgd2Uga25vdyBhcmUgaW4gdGhlIGFwcFxuICBleHBlY3QobGlzdCkudG9Db250YWluKGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1sYW1iZGFgKTtcbiAgZXhwZWN0KGxpc3QpLnRvQ29udGFpbihgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0xYCk7XG4gIGV4cGVjdChsaXN0KS50b0NvbnRhaW4oYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMmApO1xuXG4gIC8vIENoZWNrIHRoYXQgd2UgY2FuIHVzZSAnLicgYW5kIGp1c3Qgc3ludGggLHRoZSBnZW5lcmF0ZWQgYXNtXG4gIGNvbnN0IHN0YWNrVGVtcGxhdGUgPSBhd2FpdCBmaXh0dXJlLmNkayhbJy0tYXBwJywgJy4nLCAnc3ludGgnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMicpXSwge1xuICAgIGN3ZDogYXNtT3V0cHV0RGlyLFxuICB9KTtcbiAgZXhwZWN0KHN0YWNrVGVtcGxhdGUpLnRvQ29udGFpbigndG9waWMxNTJEODRBMzcnKTtcblxuICAvLyBEZXBsb3kgYSBMYW1iZGEgZnJvbSB0aGUgY29waWVkIGFzbVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnbGFtYmRhJywgeyBvcHRpb25zOiBbJy1hJywgJy4nXSwgY3dkOiBhc21PdXRwdXREaXIgfSk7XG5cbiAgLy8gUmVtb3ZlIChyZW5hbWUpIHRoZSBvcmlnaW5hbCBjdXN0b20gZG9ja2VyIGZpbGUgdGhhdCB3YXMgdXNlZCBkdXJpbmcgc3ludGguXG4gIC8vIHRoaXMgdmVyaWZpZXMgdGhhdCB0aGUgYXNzZW1seSBoYXMgYSBjb3B5IG9mIGl0IGFuZCB0aGF0IHRoZSBtYW5pZmVzdCB1c2VzXG4gIC8vIHJlbGF0aXZlIHBhdGhzIHRvIHJlZmVyZW5jZSB0byBpdC5cbiAgY29uc3QgY3VzdG9tRG9ja2VyRmlsZSA9IHBhdGguam9pbihmaXh0dXJlLmludGVnVGVzdERpciwgJ2RvY2tlcicsICdEb2NrZXJmaWxlLkN1c3RvbScpO1xuICBhd2FpdCBmcy5yZW5hbWUoY3VzdG9tRG9ja2VyRmlsZSwgYCR7Y3VzdG9tRG9ja2VyRmlsZX1+YCk7XG4gIHRyeSB7XG5cbiAgICAvLyBkZXBsb3kgYSBkb2NrZXIgaW1hZ2Ugd2l0aCBjdXN0b20gZmlsZSB3aXRob3V0IHN5bnRoICh1c2VzIGFzc2V0cylcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZG9ja2VyLXdpdGgtY3VzdG9tLWZpbGUnLCB7IG9wdGlvbnM6IFsnLWEnLCAnLiddLCBjd2Q6IGFzbU91dHB1dERpciB9KTtcblxuICB9IGZpbmFsbHkge1xuICAgIC8vIFJlbmFtZSBiYWNrIHRvIHJlc3RvcmUgZml4dHVyZSB0byBvcmlnaW5hbCBzdGF0ZVxuICAgIGF3YWl0IGZzLnJlbmFtZShgJHtjdXN0b21Eb2NrZXJGaWxlfX5gLCBjdXN0b21Eb2NrZXJGaWxlKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ3RlbXBsYXRlcyBvbiBkaXNrIGNvbnRhaW4gbWV0YWRhdGEgcmVzb3VyY2UsIGFsc28gaW4gbmVzdGVkIGFzc2VtYmxpZXMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gU3ludGggZmlyc3QsIGFuZCBzd2l0Y2ggb24gdmVyc2lvbiByZXBvcnRpbmcgYmVjYXVzZSBjZGsuanNvbiBpcyBkaXNhYmxpbmcgaXRcbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsICctLXZlcnNpb24tcmVwb3J0aW5nPXRydWUnXSk7XG5cbiAgLy8gTG9hZCB0ZW1wbGF0ZSBmcm9tIGRpc2sgZnJvbSByb290IGFzc2VtYmx5XG4gIGNvbnN0IHRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvKi1sYW1iZGEudGVtcGxhdGUuanNvbiddKTtcblxuICBleHBlY3QoSlNPTi5wYXJzZSh0ZW1wbGF0ZUNvbnRlbnRzKS5SZXNvdXJjZXMuQ0RLTWV0YWRhdGEpLnRvQmVUcnV0aHkoKTtcblxuICAvLyBMb2FkIHRlbXBsYXRlIGZyb20gbmVzdGVkIGFzc2VtYmx5XG4gIGNvbnN0IG5lc3RlZFRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvYXNzZW1ibHktKi1zdGFnZS8qLXN0YWdlLVN0YWNrSW5TdGFnZS50ZW1wbGF0ZS5qc29uJ10pO1xuXG4gIGV4cGVjdChKU09OLnBhcnNlKG5lc3RlZFRlbXBsYXRlQ29udGVudHMpLlJlc291cmNlcy5DREtNZXRhZGF0YSkudG9CZVRydXRoeSgpO1xufSkpO1xuXG5hc3luYyBmdW5jdGlvbiBsaXN0Q2hpbGRyZW4ocGFyZW50OiBzdHJpbmcsIHByZWQ6ICh4OiBzdHJpbmcpID0+IFByb21pc2U8Ym9vbGVhbj4pIHtcbiAgY29uc3QgcmV0ID0gbmV3IEFycmF5PHN0cmluZz4oKTtcbiAgZm9yIChjb25zdCBjaGlsZCBvZiBhd2FpdCBmcy5yZWFkZGlyKHBhcmVudCwgeyBlbmNvZGluZzogJ3V0Zi04JyB9KSkge1xuICAgIGNvbnN0IGZ1bGxQYXRoID0gcGF0aC5qb2luKHBhcmVudCwgY2hpbGQudG9TdHJpbmcoKSk7XG4gICAgaWYgKGF3YWl0IHByZWQoZnVsbFBhdGgpKSB7XG4gICAgICByZXQucHVzaChmdWxsUGF0aCk7XG4gICAgfVxuICB9XG4gIHJldHVybiByZXQ7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGxpc3RDaGlsZERpcnMocGFyZW50OiBzdHJpbmcpIHtcbiAgcmV0dXJuIGxpc3RDaGlsZHJlbihwYXJlbnQsIGFzeW5jIChmdWxsUGF0aDogc3RyaW5nKSA9PiAoYXdhaXQgZnMuc3RhdChmdWxsUGF0aCkpLmlzRGlyZWN0b3J5KCkpO1xufVxuIl19 \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.1/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.1/NOTES.md new file mode 100644 index 000000000..1cb31072a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.1/NOTES.md @@ -0,0 +1,3 @@ +Added a `-v` switch to the cdk executions that also needs to be +applied to the regression tests so we have a better chance +of catching sporadically failing tests in the act. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.1/cdk-helpers.js b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.1/cdk-helpers.js new file mode 100644 index 000000000..43bd06245 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.1/cdk-helpers.js @@ -0,0 +1,324 @@ +"use strict"; +var _a, _b; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.randomString = exports.rimraf = exports.shell = exports.TestFixture = exports.cloneDirectory = exports.withDefaultFixture = exports.withCdkApp = exports.withAws = void 0; +const child_process = require("child_process"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const aws_helpers_1 = require("./aws-helpers"); +const resource_pool_1 = require("./resource-pool"); +const REGIONS = process.env.AWS_REGIONS + ? process.env.AWS_REGIONS.split(',') + : [(_b = (_a = process.env.AWS_REGION) !== null && _a !== void 0 ? _a : process.env.AWS_DEFAULT_REGION) !== null && _b !== void 0 ? _b : 'us-east-1']; +process.stdout.write(`Using regions: ${REGIONS}\n`); +const REGION_POOL = new resource_pool_1.ResourcePool(REGIONS); +/** + * Higher order function to execute a block with an AWS client setup + * + * Allocate the next region from the REGION pool and dispose it afterwards. + */ +function withAws(block) { + return (context) => REGION_POOL.using(async (region) => { + const aws = await aws_helpers_1.AwsClients.forRegion(region, context.output); + await sanityCheck(aws); + return block({ ...context, aws }); + }); +} +exports.withAws = withAws; +/** + * Higher order function to execute a block with a CDK app fixture + * + * Requires an AWS client to be passed in. + * + * For backwards compatibility with existing tests (so we don't have to change + * too much) the inner block is expected to take a `TestFixture` object. + */ +function withCdkApp(block) { + return async (context) => { + const randy = randomString(); + const stackNamePrefix = `cdktest-${randy}`; + const integTestDir = path.join(os.tmpdir(), `cdk-integ-${randy}`); + context.output.write(` Stack prefix: ${stackNamePrefix}\n`); + context.output.write(` Test directory: ${integTestDir}\n`); + context.output.write(` Region: ${context.aws.region}\n`); + await cloneDirectory(path.join(__dirname, 'app'), integTestDir, context.output); + const fixture = new TestFixture(integTestDir, stackNamePrefix, context.output, context.aws); + let success = true; + try { + await fixture.shell(['npm', 'install', + '@aws-cdk/core', + '@aws-cdk/aws-sns', + '@aws-cdk/aws-iam', + '@aws-cdk/aws-lambda', + '@aws-cdk/aws-ssm', + '@aws-cdk/aws-ecr-assets', + '@aws-cdk/aws-cloudformation', + '@aws-cdk/aws-ec2']); + await ensureBootstrapped(fixture); + await block(fixture); + } + catch (e) { + success = false; + throw e; + } + finally { + await fixture.dispose(success); + } + }; +} +exports.withCdkApp = withCdkApp; +/** + * Default test fixture for most (all?) integ tests + * + * It's a composition of withAws/withCdkApp, expecting the test block to take a `TestFixture` + * object. + * + * We could have put `withAws(withCdkApp(fixture => { /... actual test here.../ }))` in every + * test declaration but centralizing it is going to make it convenient to modify in the future. + */ +function withDefaultFixture(block) { + return withAws(withCdkApp(block)); + // ^~~~~~ this is disappointing TypeScript! Feels like you should have been able to derive this. +} +exports.withDefaultFixture = withDefaultFixture; +/** + * Prepare a target dir byreplicating a source directory + */ +async function cloneDirectory(source, target, output) { + await shell(['rm', '-rf', target], { output }); + await shell(['mkdir', '-p', target], { output }); + await shell(['cp', '-R', source + '/*', target], { output }); +} +exports.cloneDirectory = cloneDirectory; +class TestFixture { + constructor(integTestDir, stackNamePrefix, output, aws) { + this.integTestDir = integTestDir; + this.stackNamePrefix = stackNamePrefix; + this.output = output; + this.aws = aws; + this.qualifier = randomString().slice(0, 10); + this.bucketsToDelete = new Array(); + } + log(s) { + this.output.write(`${s}\n`); + } + async shell(command, options = {}) { + return shell(command, { + output: this.output, + cwd: this.integTestDir, + ...options, + }); + } + async cdkDeploy(stackNames, options = {}) { + var _a, _b; + stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + const neverRequireApproval = (_a = options.neverRequireApproval) !== null && _a !== void 0 ? _a : true; + return this.cdk(['deploy', + ...(neverRequireApproval ? ['--require-approval=never'] : []), // Default to no approval in an unattended test + ...((_b = options.options) !== null && _b !== void 0 ? _b : []), ...this.fullStackName(stackNames)], options); + } + async cdkDestroy(stackNames, options = {}) { + var _a; + stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + return this.cdk(['destroy', + '-f', // We never want a prompt in an unattended test + ...((_a = options.options) !== null && _a !== void 0 ? _a : []), ...this.fullStackName(stackNames)], options); + } + async cdk(args, options = {}) { + var _a; + const verbose = (_a = options.verbose) !== null && _a !== void 0 ? _a : true; + return this.shell(['cdk', ...(verbose ? ['-v'] : []), ...args], { + ...options, + modEnv: { + AWS_REGION: this.aws.region, + AWS_DEFAULT_REGION: this.aws.region, + STACK_NAME_PREFIX: this.stackNamePrefix, + ...options.modEnv, + }, + }); + } + fullStackName(stackNames) { + if (typeof stackNames === 'string') { + return `${this.stackNamePrefix}-${stackNames}`; + } + else { + return stackNames.map(s => `${this.stackNamePrefix}-${s}`); + } + } + /** + * Append this to the list of buckets to potentially delete + * + * At the end of a test, we clean up buckets that may not have gotten destroyed + * (for whatever reason). + */ + rememberToDeleteBucket(bucketName) { + this.bucketsToDelete.push(bucketName); + } + /** + * Cleanup leftover stacks and buckets + */ + async dispose(success) { + const stacksToDelete = await this.deleteableStacks(this.stackNamePrefix); + // Bootstrap stacks have buckets that need to be cleaned + const bucketNames = stacksToDelete.map(stack => aws_helpers_1.outputFromStack('BucketName', stack)).filter(defined); + await Promise.all(bucketNames.map(b => this.aws.emptyBucket(b))); + // Bootstrap stacks have ECR repositories with images which should be deleted + const imageRepositoryNames = stacksToDelete.map(stack => aws_helpers_1.outputFromStack('ImageRepositoryName', stack)).filter(defined); + await Promise.all(imageRepositoryNames.map(r => this.aws.deleteImageRepository(r))); + await this.aws.deleteStacks(...stacksToDelete.map(s => s.StackName)); + // We might have leaked some buckets by upgrading the bootstrap stack. Be + // sure to clean everything. + for (const bucket of this.bucketsToDelete) { + await this.aws.deleteBucket(bucket); + } + // If the tests completed successfully, happily delete the fixture + // (otherwise leave it for humans to inspect) + if (success) { + rimraf(this.integTestDir); + } + } + /** + * Return the stacks starting with our testing prefix that should be deleted + */ + async deleteableStacks(prefix) { + var _a; + const statusFilter = [ + 'CREATE_IN_PROGRESS', 'CREATE_FAILED', 'CREATE_COMPLETE', + 'ROLLBACK_IN_PROGRESS', 'ROLLBACK_FAILED', 'ROLLBACK_COMPLETE', + 'DELETE_FAILED', + 'UPDATE_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_COMPLETE', 'UPDATE_ROLLBACK_IN_PROGRESS', + 'UPDATE_ROLLBACK_FAILED', + 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_ROLLBACK_COMPLETE', 'REVIEW_IN_PROGRESS', + 'IMPORT_IN_PROGRESS', 'IMPORT_COMPLETE', + 'IMPORT_ROLLBACK_IN_PROGRESS', 'IMPORT_ROLLBACK_FAILED', + 'IMPORT_ROLLBACK_COMPLETE', + ]; + const response = await this.aws.cloudFormation('describeStacks', {}); + return ((_a = response.Stacks) !== null && _a !== void 0 ? _a : []) + .filter(s => s.StackName.startsWith(prefix)) + .filter(s => statusFilter.includes(s.StackStatus)) + .filter(s => s.RootId === undefined); // Only delete parent stacks. Nested stacks are deleted in the process + } +} +exports.TestFixture = TestFixture; +/** + * Perform a one-time quick sanity check that the AWS clients has properly configured credentials + * + * If we don't do this, calls are going to fail and they'll be retried and everything will take + * forever before the user notices a simple misconfiguration. + * + * We can't check for the presence of environment variables since credentials could come from + * anywhere, so do simple account retrieval. + * + * Only do it once per process. + */ +async function sanityCheck(aws) { + if (sanityChecked === undefined) { + try { + await aws.account(); + sanityChecked = true; + } + catch (e) { + sanityChecked = false; + throw new Error(`AWS credentials probably not configured, got error: ${e.message}`); + } + } + if (!sanityChecked) { + throw new Error('AWS credentials probably not configured, see previous error'); + } +} +let sanityChecked; +/** + * Make sure that the given environment is bootstrapped + * + * Since we go striping across regions, it's going to suck doing this + * by hand so let's just mass-automate it. + */ +async function ensureBootstrapped(fixture) { + // Old-style bootstrap stack with default name + if (await fixture.aws.stackStatus('CDKToolkit') === undefined) { + await fixture.cdk(['bootstrap', `aws://${await fixture.aws.account()}/${fixture.aws.region}`]); + } +} +/** + * A shell command that does what you want + * + * Is platform-aware, handles errors nicely. + */ +async function shell(command, options = {}) { + var _a, _b; + if (options.modEnv && options.env) { + throw new Error('Use either env or modEnv but not both'); + } + (_a = options.output) === null || _a === void 0 ? void 0 : _a.write(`💻 ${command.join(' ')}\n`); + const env = (_b = options.env) !== null && _b !== void 0 ? _b : (options.modEnv ? { ...process.env, ...options.modEnv } : undefined); + const child = child_process.spawn(command[0], command.slice(1), { + ...options, + env, + // Need this for Windows where we want .cmd and .bat to be found as well. + shell: true, + stdio: ['ignore', 'pipe', 'pipe'], + }); + return new Promise((resolve, reject) => { + const stdout = new Array(); + const stderr = new Array(); + child.stdout.on('data', chunk => { + var _a; + (_a = options.output) === null || _a === void 0 ? void 0 : _a.write(chunk); + stdout.push(chunk); + }); + child.stderr.on('data', chunk => { + var _a, _b; + (_a = options.output) === null || _a === void 0 ? void 0 : _a.write(chunk); + if ((_b = options.captureStderr) !== null && _b !== void 0 ? _b : true) { + stderr.push(chunk); + } + }); + child.once('error', reject); + child.once('close', code => { + if (code === 0 || options.allowErrExit) { + resolve((Buffer.concat(stdout).toString('utf-8') + Buffer.concat(stderr).toString('utf-8')).trim()); + } + else { + reject(new Error(`'${command.join(' ')}' exited with error code ${code}`)); + } + }); + }); +} +exports.shell = shell; +function defined(x) { + return x !== undefined; +} +/** + * rm -rf reimplementation, don't want to depend on an NPM package for this + */ +function rimraf(fsPath) { + try { + const isDir = fs.lstatSync(fsPath).isDirectory(); + if (isDir) { + for (const file of fs.readdirSync(fsPath)) { + rimraf(path.join(fsPath, file)); + } + fs.rmdirSync(fsPath); + } + else { + fs.unlinkSync(fsPath); + } + } + catch (e) { + // We will survive ENOENT + if (e.code !== 'ENOENT') { + throw e; + } + } +} +exports.rimraf = rimraf; +function randomString() { + // Crazy + return Math.random().toString(36).replace(/[^a-z0-9]+/g, ''); +} +exports.randomString = randomString; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2RrLWhlbHBlcnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjZGstaGVscGVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7O0FBQUEsK0NBQStDO0FBQy9DLHlCQUF5QjtBQUN6Qix5QkFBeUI7QUFDekIsNkJBQTZCO0FBQzdCLCtDQUE0RDtBQUM1RCxtREFBK0M7QUFHL0MsTUFBTSxPQUFPLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxXQUFXO0lBQ3JDLENBQUMsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLFdBQVcsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDO0lBQ3BDLENBQUMsQ0FBQyxhQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsVUFBVSxtQ0FBSSxPQUFPLENBQUMsR0FBRyxDQUFDLGtCQUFrQixtQ0FBSSxXQUFXLENBQUMsQ0FBQztBQUU5RSxPQUFPLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxrQkFBa0IsT0FBTyxJQUFJLENBQUMsQ0FBQztBQUVwRCxNQUFNLFdBQVcsR0FBRyxJQUFJLDRCQUFZLENBQUMsT0FBTyxDQUFDLENBQUM7QUFLOUM7Ozs7R0FJRztBQUNILFNBQWdCLE9BQU8sQ0FBd0IsS0FBaUQ7SUFDOUYsT0FBTyxDQUFDLE9BQVUsRUFBRSxFQUFFLENBQUMsV0FBVyxDQUFDLEtBQUssQ0FBQyxLQUFLLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDeEQsTUFBTSxHQUFHLEdBQUcsTUFBTSx3QkFBVSxDQUFDLFNBQVMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQy9ELE1BQU0sV0FBVyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBRXZCLE9BQU8sS0FBSyxDQUFDLEVBQUUsR0FBRyxPQUFPLEVBQUUsR0FBRyxFQUFFLENBQUMsQ0FBQztJQUNwQyxDQUFDLENBQUMsQ0FBQztBQUNMLENBQUM7QUFQRCwwQkFPQztBQUVEOzs7Ozs7O0dBT0c7QUFDSCxTQUFnQixVQUFVLENBQXFDLEtBQThD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LEVBQUUsQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDLENBQUM7U0FDdEI7YUFBTTtZQUNMLEVBQUUsQ0FBQyxVQUFVLENBQUMsTUFBTSxDQUFDLENBQUM7U0FDdkI7S0FDRjtJQUFDLE9BQU8sQ0FBQyxFQUFFO1FBQ1YseUJBQXlCO1FBQ3pCLElBQUksQ0FBQyxDQUFDLElBQUksS0FBSyxRQUFRLEVBQUU7WUFBRSxNQUFNLENBQUMsQ0FBQztTQUFFO0tBQ3RDO0FBQ0gsQ0FBQztBQWhCRCx3QkFnQkM7QUFFRCxTQUFnQixZQUFZO0lBQzFCLFFBQVE7SUFDUixPQUFPLElBQUksQ0FBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsT0FBTyxDQUFDLGFBQWEsRUFBRSxFQUFFLENBQUMsQ0FBQztBQUMvRCxDQUFDO0FBSEQsb0NBR0MiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBjaGlsZF9wcm9jZXNzIGZyb20gJ2NoaWxkX3Byb2Nlc3MnO1xuaW1wb3J0ICogYXMgZnMgZnJvbSAnZnMnO1xuaW1wb3J0ICogYXMgb3MgZnJvbSAnb3MnO1xuaW1wb3J0ICogYXMgcGF0aCBmcm9tICdwYXRoJztcbmltcG9ydCB7IG91dHB1dEZyb21TdGFjaywgQXdzQ2xpZW50cyB9IGZyb20gJy4vYXdzLWhlbHBlcnMnO1xuaW1wb3J0IHsgUmVzb3VyY2VQb29sIH0gZnJvbSAnLi9yZXNvdXJjZS1wb29sJztcbmltcG9ydCB7IFRlc3RDb250ZXh0IH0gZnJvbSAnLi90ZXN0LWhlbHBlcnMnO1xuXG5jb25zdCBSRUdJT05TID0gcHJvY2Vzcy5lbnYuQVdTX1JFR0lPTlNcbiAgPyBwcm9jZXNzLmVudi5BV1NfUkVHSU9OUy5zcGxpdCgnLCcpXG4gIDogW3Byb2Nlc3MuZW52LkFXU19SRUdJT04gPz8gcHJvY2Vzcy5lbnYuQVdTX0RFRkFVTFRfUkVHSU9OID8/ICd1cy1lYXN0LTEnXTtcblxucHJvY2Vzcy5zdGRvdXQud3JpdGUoYFVzaW5nIHJlZ2lvbnM6ICR7UkVHSU9OU31cXG5gKTtcblxuY29uc3QgUkVHSU9OX1BPT0wgPSBuZXcgUmVzb3VyY2VQb29sKFJFR0lPTlMpO1xuXG5cbmV4cG9ydCB0eXBlIEF3c0NvbnRleHQgPSB7IHJlYWRvbmx5IGF3czogQXdzQ2xpZW50cyB9O1xuXG4vKipcbiAqIEhpZ2hlciBvcmRlciBmdW5jdGlvbiB0byBleGVjdXRlIGEgYmxvY2sgd2l0aCBhbiBBV1MgY2xpZW50IHNldHVwXG4gKlxuICogQWxsb2NhdGUgdGhlIG5leHQgcmVnaW9uIGZyb20gdGhlIFJFR0lPTiBwb29sIGFuZCBkaXNwb3NlIGl0IGFmdGVyd2FyZHMuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB3aXRoQXdzPEEgZXh0ZW5kcyBUZXN0Q29udGV4dD4oYmxvY2s6IChjb250ZXh0OiBBICYgQXdzQ29udGV4dCkgPT4gUHJvbWlzZTx2b2lkPikge1xuICByZXR1cm4gKGNvbnRleHQ6IEEpID0+IFJFR0lPTl9QT09MLnVzaW5nKGFzeW5jIChyZWdpb24pID0+IHtcbiAgICBjb25zdCBhd3MgPSBhd2FpdCBBd3NDbGllbnRzLmZvclJlZ2lvbihyZWdpb24sIGNvbnRleHQub3V0cHV0KTtcbiAgICBhd2FpdCBzYW5pdHlDaGVjayhhd3MpO1xuXG4gICAgcmV0dXJuIGJsb2NrKHsgLi4uY29udGV4dCwgYXdzIH0pO1xuICB9KTtcbn1cblxuLyoqXG4gKiBIaWdoZXIgb3JkZXIgZnVuY3Rpb24gdG8gZXhlY3V0ZSBhIGJsb2NrIHdpdGggYSBDREsgYXBwIGZpeHR1cmVcbiAqXG4gKiBSZXF1aXJlcyBhbiBBV1MgY2xpZW50IHRvIGJlIHBhc3NlZCBpbi5cbiAqXG4gKiBGb3IgYmFja3dhcmRzIGNvbXBhdGliaWxpdHkgd2l0aCBleGlzdGluZyB0ZXN0cyAoc28gd2UgZG9uJ3QgaGF2ZSB0byBjaGFuZ2VcbiAqIHRvbyBtdWNoKSB0aGUgaW5uZXIgYmxvY2sgaXMgZXhwZWN0ZWQgdG8gdGFrZSBhIGBUZXN0Rml4dHVyZWAgb2JqZWN0LlxuICovXG5leHBvcnQgZnVuY3Rpb24gd2l0aENka0FwcDxBIGV4dGVuZHMgVGVzdENvbnRleHQgJiBBd3NDb250ZXh0PihibG9jazogKGNvbnRleHQ6IFRlc3RGaXh0dXJlKSA9PiBQcm9taXNlPHZvaWQ+KSB7XG4gIHJldHVybiBhc3luYyAoY29udGV4dDogQSkgPT4ge1xuICAgIGNvbnN0IHJhbmR5ID0gcmFuZG9tU3RyaW5nKCk7XG4gICAgY29uc3Qgc3RhY2tOYW1lUHJlZml4ID0gYGNka3Rlc3QtJHtyYW5keX1gO1xuICAgIGNvbnN0IGludGVnVGVzdERpciA9IHBhdGguam9pbihvcy50bXBkaXIoKSwgYGNkay1pbnRlZy0ke3JhbmR5fWApO1xuXG4gICAgY29udGV4dC5vdXRwdXQud3JpdGUoYCBTdGFjayBwcmVmaXg6ICAgJHtzdGFja05hbWVQcmVmaXh9XFxuYCk7XG4gICAgY29udGV4dC5vdXRwdXQud3JpdGUoYCBUZXN0IGRpcmVjdG9yeTogJHtpbnRlZ1Rlc3REaXJ9XFxuYCk7XG4gICAgY29udGV4dC5vdXRwdXQud3JpdGUoYCBSZWdpb246ICAgICAgICAgJHtjb250ZXh0LmF3cy5yZWdpb259XFxuYCk7XG5cbiAgICBhd2FpdCBjbG9uZURpcmVjdG9yeShwYXRoLmpvaW4oX19kaXJuYW1lLCAnYXBwJyksIGludGVnVGVzdERpciwgY29udGV4dC5vdXRwdXQpO1xuICAgIGNvbnN0IGZpeHR1cmUgPSBuZXcgVGVzdEZpeHR1cmUoXG4gICAgICBpbnRlZ1Rlc3REaXIsXG4gICAgICBzdGFja05hbWVQcmVmaXgsXG4gICAgICBjb250ZXh0Lm91dHB1dCxcbiAgICAgIGNvbnRleHQuYXdzKTtcblxuICAgIGxldCBzdWNjZXNzID0gdHJ1ZTtcbiAgICB0cnkge1xuICAgICAgYXdhaXQgZml4dHVyZS5zaGVsbChbJ25wbScsICdpbnN0YWxsJyxcbiAgICAgICAgJ0Bhd3MtY2RrL2NvcmUnLFxuICAgICAgICAnQGF3cy1jZGsvYXdzLXNucycsXG4gICAgICAgICdAYXdzLWNkay9hd3MtaWFtJyxcbiAgICAgICAgJ0Bhd3MtY2RrL2F3cy1sYW1iZGEnLFxuICAgICAgICAnQGF3cy1jZGsvYXdzLXNzbScsXG4gICAgICAgICdAYXdzLWNkay9hd3MtZWNyLWFzc2V0cycsXG4gICAgICAgICdAYXdzLWNkay9hd3MtY2xvdWRmb3JtYXRpb24nLFxuICAgICAgICAnQGF3cy1jZGsvYXdzLWVjMiddKTtcblxuICAgICAgYXdhaXQgZW5zdXJlQm9vdHN0cmFwcGVkKGZpeHR1cmUpO1xuXG4gICAgICBhd2FpdCBibG9jayhmaXh0dXJlKTtcbiAgICB9IGNhdGNoIChlKSB7XG4gICAgICBzdWNjZXNzID0gZmFsc2U7XG4gICAgICB0aHJvdyBlO1xuICAgIH0gZmluYWxseSB7XG4gICAgICBhd2FpdCBmaXh0dXJlLmRpc3Bvc2Uoc3VjY2Vzcyk7XG4gICAgfVxuICB9O1xufVxuXG4vKipcbiAqIERlZmF1bHQgdGVzdCBmaXh0dXJlIGZvciBtb3N0IChhbGw/KSBpbnRlZyB0ZXN0c1xuICpcbiAqIEl0J3MgYSBjb21wb3NpdGlvbiBvZiB3aXRoQXdzL3dpdGhDZGtBcHAsIGV4cGVjdGluZyB0aGUgdGVzdCBibG9jayB0byB0YWtlIGEgYFRlc3RGaXh0dXJlYFxuICogb2JqZWN0LlxuICpcbiAqIFdlIGNvdWxkIGhhdmUgcHV0IGB3aXRoQXdzKHdpdGhDZGtBcHAoZml4dHVyZSA9PiB7IC8uLi4gYWN0dWFsIHRlc3QgaGVyZS4uLi8gfSkpYCBpbiBldmVyeVxuICogdGVzdCBkZWNsYXJhdGlvbiBidXQgY2VudHJhbGl6aW5nIGl0IGlzIGdvaW5nIHRvIG1ha2UgaXQgY29udmVuaWVudCB0byBtb2RpZnkgaW4gdGhlIGZ1dHVyZS5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHdpdGhEZWZhdWx0Rml4dHVyZShibG9jazogKGNvbnRleHQ6IFRlc3RGaXh0dXJlKSA9PiBQcm9taXNlPHZvaWQ+KSB7XG4gIHJldHVybiB3aXRoQXdzPFRlc3RDb250ZXh0Pih3aXRoQ2RrQXBwKGJsb2NrKSk7XG4gIC8vICAgICAgICAgICAgICBefn5+fn4gdGhpcyBpcyBkaXNhcHBvaW50aW5nIFR5cGVTY3JpcHQhIEZlZWxzIGxpa2UgeW91IHNob3VsZCBoYXZlIGJlZW4gYWJsZSB0byBkZXJpdmUgdGhpcy5cbn1cblxuZXhwb3J0IGludGVyZmFjZSBTaGVsbE9wdGlvbnMgZXh0ZW5kcyBjaGlsZF9wcm9jZXNzLlNwYXduT3B0aW9ucyB7XG4gIC8qKlxuICAgKiBQcm9wZXJ0aWVzIHRvIGFkZCB0byAnZW52J1xuICAgKi9cbiAgbW9kRW52PzogUmVjb3JkPHN0cmluZywgc3RyaW5nPjtcblxuICAvKipcbiAgICogRG9uJ3QgZmFpbCB3aGVuIGV4aXRpbmcgd2l0aCBhbiBlcnJvclxuICAgKlxuICAgKiBAZGVmYXVsdCBmYWxzZVxuICAgKi9cbiAgYWxsb3dFcnJFeGl0PzogYm9vbGVhbjtcblxuICAvKipcbiAgICogV2hldGhlciB0byBjYXB0dXJlIHN0ZGVyclxuICAgKlxuICAgKiBAZGVmYXVsdCB0cnVlXG4gICAqL1xuICBjYXB0dXJlU3RkZXJyPzogYm9vbGVhbjtcblxuICAvKipcbiAgICogUGFzcyBvdXRwdXQgaGVyZVxuICAgKi9cbiAgb3V0cHV0PzogTm9kZUpTLldyaXRhYmxlU3RyZWFtO1xufVxuXG5leHBvcnQgaW50ZXJmYWNlIENka0NsaU9wdGlvbnMgZXh0ZW5kcyBTaGVsbE9wdGlvbnMge1xuICBvcHRpb25zPzogc3RyaW5nW107XG4gIG5ldmVyUmVxdWlyZUFwcHJvdmFsPzogYm9vbGVhbjtcbiAgdmVyYm9zZT86IGJvb2xlYW47XG59XG5cbi8qKlxuICogUHJlcGFyZSBhIHRhcmdldCBkaXIgYnlyZXBsaWNhdGluZyBhIHNvdXJjZSBkaXJlY3RvcnlcbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIGNsb25lRGlyZWN0b3J5KHNvdXJjZTogc3RyaW5nLCB0YXJnZXQ6IHN0cmluZywgb3V0cHV0PzogTm9kZUpTLldyaXRhYmxlU3RyZWFtKSB7XG4gIGF3YWl0IHNoZWxsKFsncm0nLCAnLXJmJywgdGFyZ2V0XSwgeyBvdXRwdXQgfSk7XG4gIGF3YWl0IHNoZWxsKFsnbWtkaXInLCAnLXAnLCB0YXJnZXRdLCB7IG91dHB1dCB9KTtcbiAgYXdhaXQgc2hlbGwoWydjcCcsICctUicsIHNvdXJjZSArICcvKicsIHRhcmdldF0sIHsgb3V0cHV0IH0pO1xufVxuXG5leHBvcnQgY2xhc3MgVGVzdEZpeHR1cmUge1xuICBwdWJsaWMgcmVhZG9ubHkgcXVhbGlmaWVyID0gcmFuZG9tU3RyaW5nKCkuc3Vic3RyKDAsIDEwKTtcbiAgcHJpdmF0ZSByZWFkb25seSBidWNrZXRzVG9EZWxldGUgPSBuZXcgQXJyYXk8c3RyaW5nPigpO1xuXG4gIGNvbnN0cnVjdG9yKFxuICAgIHB1YmxpYyByZWFkb25seSBpbnRlZ1Rlc3REaXI6IHN0cmluZyxcbiAgICBwdWJsaWMgcmVhZG9ubHkgc3RhY2tOYW1lUHJlZml4OiBzdHJpbmcsXG4gICAgcHVibGljIHJlYWRvbmx5IG91dHB1dDogTm9kZUpTLldyaXRhYmxlU3RyZWFtLFxuICAgIHB1YmxpYyByZWFkb25seSBhd3M6IEF3c0NsaWVudHMpIHtcbiAgfVxuXG4gIHB1YmxpYyBsb2coczogc3RyaW5nKSB7XG4gICAgdGhpcy5vdXRwdXQud3JpdGUoYCR7c31cXG5gKTtcbiAgfVxuXG4gIHB1YmxpYyBhc3luYyBzaGVsbChjb21tYW5kOiBzdHJpbmdbXSwgb3B0aW9uczogT21pdDxTaGVsbE9wdGlvbnMsICdjd2QnfCdvdXRwdXQnPiA9IHt9KTogUHJvbWlzZTxzdHJpbmc+IHtcbiAgICByZXR1cm4gc2hlbGwoY29tbWFuZCwge1xuICAgICAgb3V0cHV0OiB0aGlzLm91dHB1dCxcbiAgICAgIGN3ZDogdGhpcy5pbnRlZ1Rlc3REaXIsXG4gICAgICAuLi5vcHRpb25zLFxuICAgIH0pO1xuICB9XG5cbiAgcHVibGljIGFzeW5jIGNka0RlcGxveShzdGFja05hbWVzOiBzdHJpbmcgfCBzdHJpbmdbXSwgb3B0aW9uczogQ2RrQ2xpT3B0aW9ucyA9IHt9KSB7XG4gICAgc3RhY2tOYW1lcyA9IHR5cGVvZiBzdGFja05hbWVzID09PSAnc3RyaW5nJyA/IFtzdGFja05hbWVzXSA6IHN0YWNrTmFtZXM7XG5cbiAgICBjb25zdCBuZXZlclJlcXVpcmVBcHByb3ZhbCA9IG9wdGlvbnMubmV2ZXJSZXF1aXJlQXBwcm92YWwgPz8gdHJ1ZTtcblxuICAgIHJldHVybiB0aGlzLmNkayhbJ2RlcGxveScsXG4gICAgICAuLi4obmV2ZXJSZXF1aXJlQXBwcm92YWwgPyBbJy0tcmVxdWlyZS1hcHByb3ZhbD1uZXZlciddIDogW10pLCAvLyBEZWZhdWx0IHRvIG5vIGFwcHJvdmFsIGluIGFuIHVuYXR0ZW5kZWQgdGVzdFxuICAgICAgLi4uKG9wdGlvbnMub3B0aW9ucyA/PyBbXSksXG4gICAgICAuLi50aGlzLmZ1bGxTdGFja05hbWUoc3RhY2tOYW1lcyldLCBvcHRpb25zKTtcbiAgfVxuXG4gIHB1YmxpYyBhc3luYyBjZGtEZXN0cm95KHN0YWNrTmFtZXM6IHN0cmluZyB8IHN0cmluZ1tdLCBvcHRpb25zOiBDZGtDbGlPcHRpb25zID0ge30pIHtcbiAgICBzdGFja05hbWVzID0gdHlwZW9mIHN0YWNrTmFtZXMgPT09ICdzdHJpbmcnID8gW3N0YWNrTmFtZXNdIDogc3RhY2tOYW1lcztcblxuICAgIHJldHVybiB0aGlzLmNkayhbJ2Rlc3Ryb3knLFxuICAgICAgJy1mJywgLy8gV2UgbmV2ZXIgd2FudCBhIHByb21wdCBpbiBhbiB1bmF0dGVuZGVkIHRlc3RcbiAgICAgIC4uLihvcHRpb25zLm9wdGlvbnMgPz8gW10pLFxuICAgICAgLi4udGhpcy5mdWxsU3RhY2tOYW1lKHN0YWNrTmFtZXMpXSwgb3B0aW9ucyk7XG4gIH1cblxuICBwdWJsaWMgYXN5bmMgY2RrKGFyZ3M6IHN0cmluZ1tdLCBvcHRpb25zOiBDZGtDbGlPcHRpb25zID0ge30pIHtcbiAgICBjb25zdCB2ZXJib3NlID0gb3B0aW9ucy52ZXJib3NlID8/IHRydWU7XG5cbiAgICByZXR1cm4gdGhpcy5zaGVsbChbJ2NkaycsIC4uLih2ZXJib3NlID8gWyctdiddIDogW10pLCAuLi5hcmdzXSwge1xuICAgICAgLi4ub3B0aW9ucyxcbiAgICAgIG1vZEVudjoge1xuICAgICAgICBBV1NfUkVHSU9OOiB0aGlzLmF3cy5yZWdpb24sXG4gICAgICAgIEFXU19ERUZBVUxUX1JFR0lPTjogdGhpcy5hd3MucmVnaW9uLFxuICAgICAgICBTVEFDS19OQU1FX1BSRUZJWDogdGhpcy5zdGFja05hbWVQcmVmaXgsXG4gICAgICAgIC4uLm9wdGlvbnMubW9kRW52LFxuICAgICAgfSxcbiAgICB9KTtcbiAgfVxuXG4gIHB1YmxpYyBmdWxsU3RhY2tOYW1lKHN0YWNrTmFtZTogc3RyaW5nKTogc3RyaW5nO1xuICBwdWJsaWMgZnVsbFN0YWNrTmFtZShzdGFja05hbWVzOiBzdHJpbmdbXSk6IHN0cmluZ1tdO1xuICBwdWJsaWMgZnVsbFN0YWNrTmFtZShzdGFja05hbWVzOiBzdHJpbmcgfCBzdHJpbmdbXSk6IHN0cmluZyB8IHN0cmluZ1tdIHtcbiAgICBpZiAodHlwZW9mIHN0YWNrTmFtZXMgPT09ICdzdHJpbmcnKSB7XG4gICAgICByZXR1cm4gYCR7dGhpcy5zdGFja05hbWVQcmVmaXh9LSR7c3RhY2tOYW1lc31gO1xuICAgIH0gZWxzZSB7XG4gICAgICByZXR1cm4gc3RhY2tOYW1lcy5tYXAocyA9PiBgJHt0aGlzLnN0YWNrTmFtZVByZWZpeH0tJHtzfWApO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBBcHBlbmQgdGhpcyB0byB0aGUgbGlzdCBvZiBidWNrZXRzIHRvIHBvdGVudGlhbGx5IGRlbGV0ZVxuICAgKlxuICAgKiBBdCB0aGUgZW5kIG9mIGEgdGVzdCwgd2UgY2xlYW4gdXAgYnVja2V0cyB0aGF0IG1heSBub3QgaGF2ZSBnb3R0ZW4gZGVzdHJveWVkXG4gICAqIChmb3Igd2hhdGV2ZXIgcmVhc29uKS5cbiAgICovXG4gIHB1YmxpYyByZW1lbWJlclRvRGVsZXRlQnVja2V0KGJ1Y2tldE5hbWU6IHN0cmluZykge1xuICAgIHRoaXMuYnVja2V0c1RvRGVsZXRlLnB1c2goYnVja2V0TmFtZSk7XG4gIH1cblxuICAvKipcbiAgICogQ2xlYW51cCBsZWZ0b3ZlciBzdGFja3MgYW5kIGJ1Y2tldHNcbiAgICovXG4gIHB1YmxpYyBhc3luYyBkaXNwb3NlKHN1Y2Nlc3M6IGJvb2xlYW4pIHtcbiAgICBjb25zdCBzdGFja3NUb0RlbGV0ZSA9IGF3YWl0IHRoaXMuZGVsZXRlYWJsZVN0YWNrcyh0aGlzLnN0YWNrTmFtZVByZWZpeCk7XG5cbiAgICAvLyBCb290c3RyYXAgc3RhY2tzIGhhdmUgYnVja2V0cyB0aGF0IG5lZWQgdG8gYmUgY2xlYW5lZFxuICAgIGNvbnN0IGJ1Y2tldE5hbWVzID0gc3RhY2tzVG9EZWxldGUubWFwKHN0YWNrID0+IG91dHB1dEZyb21TdGFjaygnQnVja2V0TmFtZScsIHN0YWNrKSkuZmlsdGVyKGRlZmluZWQpO1xuICAgIGF3YWl0IFByb21pc2UuYWxsKGJ1Y2tldE5hbWVzLm1hcChiID0+IHRoaXMuYXdzLmVtcHR5QnVja2V0KGIpKSk7XG5cbiAgICAvLyBCb290c3RyYXAgc3RhY2tzIGhhdmUgRUNSIHJlcG9zaXRvcmllcyB3aXRoIGltYWdlcyB3aGljaCBzaG91bGQgYmUgZGVsZXRlZFxuICAgIGNvbnN0IGltYWdlUmVwb3NpdG9yeU5hbWVzID0gc3RhY2tzVG9EZWxldGUubWFwKHN0YWNrID0+IG91dHB1dEZyb21TdGFjaygnSW1hZ2VSZXBvc2l0b3J5TmFtZScsIHN0YWNrKSkuZmlsdGVyKGRlZmluZWQpO1xuICAgIGF3YWl0IFByb21pc2UuYWxsKGltYWdlUmVwb3NpdG9yeU5hbWVzLm1hcChyID0+IHRoaXMuYXdzLmRlbGV0ZUltYWdlUmVwb3NpdG9yeShyKSkpO1xuXG4gICAgYXdhaXQgdGhpcy5hd3MuZGVsZXRlU3RhY2tzKC4uLnN0YWNrc1RvRGVsZXRlLm1hcChzID0+IHMuU3RhY2tOYW1lKSk7XG5cbiAgICAvLyBXZSBtaWdodCBoYXZlIGxlYWtlZCBzb21lIGJ1Y2tldHMgYnkgdXBncmFkaW5nIHRoZSBib290c3RyYXAgc3RhY2suIEJlXG4gICAgLy8gc3VyZSB0byBjbGVhbiBldmVyeXRoaW5nLlxuICAgIGZvciAoY29uc3QgYnVja2V0IG9mIHRoaXMuYnVja2V0c1RvRGVsZXRlKSB7XG4gICAgICBhd2FpdCB0aGlzLmF3cy5kZWxldGVCdWNrZXQoYnVja2V0KTtcbiAgICB9XG5cbiAgICAvLyBJZiB0aGUgdGVzdHMgY29tcGxldGVkIHN1Y2Nlc3NmdWxseSwgaGFwcGlseSBkZWxldGUgdGhlIGZpeHR1cmVcbiAgICAvLyAob3RoZXJ3aXNlIGxlYXZlIGl0IGZvciBodW1hbnMgdG8gaW5zcGVjdClcbiAgICBpZiAoc3VjY2Vzcykge1xuICAgICAgcmltcmFmKHRoaXMuaW50ZWdUZXN0RGlyKTtcbiAgICB9XG4gIH1cblxuICAvKipcbiAgICogUmV0dXJuIHRoZSBzdGFja3Mgc3RhcnRpbmcgd2l0aCBvdXIgdGVzdGluZyBwcmVmaXggdGhhdCBzaG91bGQgYmUgZGVsZXRlZFxuICAgKi9cbiAgcHJpdmF0ZSBhc3luYyBkZWxldGVhYmxlU3RhY2tzKHByZWZpeDogc3RyaW5nKTogUHJvbWlzZTxBV1MuQ2xvdWRGb3JtYXRpb24uU3RhY2tbXT4ge1xuICAgIGNvbnN0IHN0YXR1c0ZpbHRlciA9IFtcbiAgICAgICdDUkVBVEVfSU5fUFJPR1JFU1MnLCAnQ1JFQVRFX0ZBSUxFRCcsICdDUkVBVEVfQ09NUExFVEUnLFxuICAgICAgJ1JPTExCQUNLX0lOX1BST0dSRVNTJywgJ1JPTExCQUNLX0ZBSUxFRCcsICdST0xMQkFDS19DT01QTEVURScsXG4gICAgICAnREVMRVRFX0ZBSUxFRCcsXG4gICAgICAnVVBEQVRFX0lOX1BST0dSRVNTJywgJ1VQREFURV9DT01QTEVURV9DTEVBTlVQX0lOX1BST0dSRVNTJyxcbiAgICAgICdVUERBVEVfQ09NUExFVEUnLCAnVVBEQVRFX1JPTExCQUNLX0lOX1BST0dSRVNTJyxcbiAgICAgICdVUERBVEVfUk9MTEJBQ0tfRkFJTEVEJyxcbiAgICAgICdVUERBVEVfUk9MTEJBQ0tfQ09NUExFVEVfQ0xFQU5VUF9JTl9QUk9HUkVTUycsXG4gICAgICAnVVBEQVRFX1JPTExCQUNLX0NPTVBMRVRFJywgJ1JFVklFV19JTl9QUk9HUkVTUycsXG4gICAgICAnSU1QT1JUX0lOX1BST0dSRVNTJywgJ0lNUE9SVF9DT01QTEVURScsXG4gICAgICAnSU1QT1JUX1JPTExCQUNLX0lOX1BST0dSRVNTJywgJ0lNUE9SVF9ST0xMQkFDS19GQUlMRUQnLFxuICAgICAgJ0lNUE9SVF9ST0xMQkFDS19DT01QTEVURScsXG4gICAgXTtcblxuICAgIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgdGhpcy5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge30pO1xuXG4gICAgcmV0dXJuIChyZXNwb25zZS5TdGFja3MgPz8gW10pXG4gICAgICAuZmlsdGVyKHMgPT4gcy5TdGFja05hbWUuc3RhcnRzV2l0aChwcmVmaXgpKVxuICAgICAgLmZpbHRlcihzID0+IHN0YXR1c0ZpbHRlci5pbmNsdWRlcyhzLlN0YWNrU3RhdHVzKSlcbiAgICAgIC5maWx0ZXIocyA9PiBzLlJvb3RJZCA9PT0gdW5kZWZpbmVkKTsgLy8gT25seSBkZWxldGUgcGFyZW50IHN0YWNrcy4gTmVzdGVkIHN0YWNrcyBhcmUgZGVsZXRlZCBpbiB0aGUgcHJvY2Vzc1xuICB9XG59XG5cbi8qKlxuICogUGVyZm9ybSBhIG9uZS10aW1lIHF1aWNrIHNhbml0eSBjaGVjayB0aGF0IHRoZSBBV1MgY2xpZW50cyBoYXMgcHJvcGVybHkgY29uZmlndXJlZCBjcmVkZW50aWFsc1xuICpcbiAqIElmIHdlIGRvbid0IGRvIHRoaXMsIGNhbGxzIGFyZSBnb2luZyB0byBmYWlsIGFuZCB0aGV5J2xsIGJlIHJldHJpZWQgYW5kIGV2ZXJ5dGhpbmcgd2lsbCB0YWtlXG4gKiBmb3JldmVyIGJlZm9yZSB0aGUgdXNlciBub3RpY2VzIGEgc2ltcGxlIG1pc2NvbmZpZ3VyYXRpb24uXG4gKlxuICogV2UgY2FuJ3QgY2hlY2sgZm9yIHRoZSBwcmVzZW5jZSBvZiBlbnZpcm9ubWVudCB2YXJpYWJsZXMgc2luY2UgY3JlZGVudGlhbHMgY291bGQgY29tZSBmcm9tXG4gKiBhbnl3aGVyZSwgc28gZG8gc2ltcGxlIGFjY291bnQgcmV0cmlldmFsLlxuICpcbiAqIE9ubHkgZG8gaXQgb25jZSBwZXIgcHJvY2Vzcy5cbiAqL1xuYXN5bmMgZnVuY3Rpb24gc2FuaXR5Q2hlY2soYXdzOiBBd3NDbGllbnRzKSB7XG4gIGlmIChzYW5pdHlDaGVja2VkID09PSB1bmRlZmluZWQpIHtcbiAgICB0cnkge1xuICAgICAgYXdhaXQgYXdzLmFjY291bnQoKTtcbiAgICAgIHNhbml0eUNoZWNrZWQgPSB0cnVlO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgIHNhbml0eUNoZWNrZWQgPSBmYWxzZTtcbiAgICAgIHRocm93IG5ldyBFcnJvcihgQVdTIGNyZWRlbnRpYWxzIHByb2JhYmx5IG5vdCBjb25maWd1cmVkLCBnb3QgZXJyb3I6ICR7ZS5tZXNzYWdlfWApO1xuICAgIH1cbiAgfVxuICBpZiAoIXNhbml0eUNoZWNrZWQpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ0FXUyBjcmVkZW50aWFscyBwcm9iYWJseSBub3QgY29uZmlndXJlZCwgc2VlIHByZXZpb3VzIGVycm9yJyk7XG4gIH1cbn1cbmxldCBzYW5pdHlDaGVja2VkOiBib29sZWFuIHwgdW5kZWZpbmVkO1xuXG4vKipcbiAqIE1ha2Ugc3VyZSB0aGF0IHRoZSBnaXZlbiBlbnZpcm9ubWVudCBpcyBib290c3RyYXBwZWRcbiAqXG4gKiBTaW5jZSB3ZSBnbyBzdHJpcGluZyBhY3Jvc3MgcmVnaW9ucywgaXQncyBnb2luZyB0byBzdWNrIGRvaW5nIHRoaXNcbiAqIGJ5IGhhbmQgc28gbGV0J3MganVzdCBtYXNzLWF1dG9tYXRlIGl0LlxuICovXG5hc3luYyBmdW5jdGlvbiBlbnN1cmVCb290c3RyYXBwZWQoZml4dHVyZTogVGVzdEZpeHR1cmUpIHtcbiAgLy8gT2xkLXN0eWxlIGJvb3RzdHJhcCBzdGFjayB3aXRoIGRlZmF1bHQgbmFtZVxuICBpZiAoYXdhaXQgZml4dHVyZS5hd3Muc3RhY2tTdGF0dXMoJ0NES1Rvb2xraXQnKSA9PT0gdW5kZWZpbmVkKSB7XG4gICAgYXdhaXQgZml4dHVyZS5jZGsoWydib290c3RyYXAnLCBgYXdzOi8vJHthd2FpdCBmaXh0dXJlLmF3cy5hY2NvdW50KCl9LyR7Zml4dHVyZS5hd3MucmVnaW9ufWBdKTtcbiAgfVxufVxuXG4vKipcbiAqIEEgc2hlbGwgY29tbWFuZCB0aGF0IGRvZXMgd2hhdCB5b3Ugd2FudFxuICpcbiAqIElzIHBsYXRmb3JtLWF3YXJlLCBoYW5kbGVzIGVycm9ycyBuaWNlbHkuXG4gKi9cbmV4cG9ydCBhc3luYyBmdW5jdGlvbiBzaGVsbChjb21tYW5kOiBzdHJpbmdbXSwgb3B0aW9uczogU2hlbGxPcHRpb25zID0ge30pOiBQcm9taXNlPHN0cmluZz4ge1xuICBpZiAob3B0aW9ucy5tb2RFbnYgJiYgb3B0aW9ucy5lbnYpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ1VzZSBlaXRoZXIgZW52IG9yIG1vZEVudiBidXQgbm90IGJvdGgnKTtcbiAgfVxuXG4gIG9wdGlvbnMub3V0cHV0Py53cml0ZShg8J+SuyAke2NvbW1hbmQuam9pbignICcpfVxcbmApO1xuXG4gIGNvbnN0IGVudiA9IG9wdGlvbnMuZW52ID8/IChvcHRpb25zLm1vZEVudiA/IHsgLi4ucHJvY2Vzcy5lbnYsIC4uLm9wdGlvbnMubW9kRW52IH0gOiB1bmRlZmluZWQpO1xuXG4gIGNvbnN0IGNoaWxkID0gY2hpbGRfcHJvY2Vzcy5zcGF3bihjb21tYW5kWzBdLCBjb21tYW5kLnNsaWNlKDEpLCB7XG4gICAgLi4ub3B0aW9ucyxcbiAgICBlbnYsXG4gICAgLy8gTmVlZCB0aGlzIGZvciBXaW5kb3dzIHdoZXJlIHdlIHdhbnQgLmNtZCBhbmQgLmJhdCB0byBiZSBmb3VuZCBhcyB3ZWxsLlxuICAgIHNoZWxsOiB0cnVlLFxuICAgIHN0ZGlvOiBbJ2lnbm9yZScsICdwaXBlJywgJ3BpcGUnXSxcbiAgfSk7XG5cbiAgcmV0dXJuIG5ldyBQcm9taXNlPHN0cmluZz4oKHJlc29sdmUsIHJlamVjdCkgPT4ge1xuICAgIGNvbnN0IHN0ZG91dCA9IG5ldyBBcnJheTxCdWZmZXI+KCk7XG4gICAgY29uc3Qgc3RkZXJyID0gbmV3IEFycmF5PEJ1ZmZlcj4oKTtcblxuICAgIGNoaWxkLnN0ZG91dCEub24oJ2RhdGEnLCBjaHVuayA9PiB7XG4gICAgICBvcHRpb25zLm91dHB1dD8ud3JpdGUoY2h1bmspO1xuICAgICAgc3Rkb3V0LnB1c2goY2h1bmspO1xuICAgIH0pO1xuXG4gICAgY2hpbGQuc3RkZXJyIS5vbignZGF0YScsIGNodW5rID0+IHtcbiAgICAgIG9wdGlvbnMub3V0cHV0Py53cml0ZShjaHVuayk7XG4gICAgICBpZiAob3B0aW9ucy5jYXB0dXJlU3RkZXJyID8/IHRydWUpIHtcbiAgICAgICAgc3RkZXJyLnB1c2goY2h1bmspO1xuICAgICAgfVxuICAgIH0pO1xuXG4gICAgY2hpbGQub25jZSgnZXJyb3InLCByZWplY3QpO1xuXG4gICAgY2hpbGQub25jZSgnY2xvc2UnLCBjb2RlID0+IHtcbiAgICAgIGlmIChjb2RlID09PSAwIHx8IG9wdGlvbnMuYWxsb3dFcnJFeGl0KSB7XG4gICAgICAgIHJlc29sdmUoKEJ1ZmZlci5jb25jYXQoc3Rkb3V0KS50b1N0cmluZygndXRmLTgnKSArIEJ1ZmZlci5jb25jYXQoc3RkZXJyKS50b1N0cmluZygndXRmLTgnKSkudHJpbSgpKTtcbiAgICAgIH0gZWxzZSB7XG4gICAgICAgIHJlamVjdChuZXcgRXJyb3IoYCcke2NvbW1hbmQuam9pbignICcpfScgZXhpdGVkIHdpdGggZXJyb3IgY29kZSAke2NvZGV9YCkpO1xuICAgICAgfVxuICAgIH0pO1xuICB9KTtcbn1cblxuZnVuY3Rpb24gZGVmaW5lZDxBPih4OiBBKTogeCBpcyBOb25OdWxsYWJsZTxBPiB7XG4gIHJldHVybiB4ICE9PSB1bmRlZmluZWQ7XG59XG5cbi8qKlxuICogcm0gLXJmIHJlaW1wbGVtZW50YXRpb24sIGRvbid0IHdhbnQgdG8gZGVwZW5kIG9uIGFuIE5QTSBwYWNrYWdlIGZvciB0aGlzXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiByaW1yYWYoZnNQYXRoOiBzdHJpbmcpIHtcbiAgdHJ5IHtcbiAgICBjb25zdCBpc0RpciA9IGZzLmxzdGF0U3luYyhmc1BhdGgpLmlzRGlyZWN0b3J5KCk7XG5cbiAgICBpZiAoaXNEaXIpIHtcbiAgICAgIGZvciAoY29uc3QgZmlsZSBvZiBmcy5yZWFkZGlyU3luYyhmc1BhdGgpKSB7XG4gICAgICAgIHJpbXJhZihwYXRoLmpvaW4oZnNQYXRoLCBmaWxlKSk7XG4gICAgICB9XG4gICAgICBmcy5ybWRpclN5bmMoZnNQYXRoKTtcbiAgICB9IGVsc2Uge1xuICAgICAgZnMudW5saW5rU3luYyhmc1BhdGgpO1xuICAgIH1cbiAgfSBjYXRjaCAoZSkge1xuICAgIC8vIFdlIHdpbGwgc3Vydml2ZSBFTk9FTlRcbiAgICBpZiAoZS5jb2RlICE9PSAnRU5PRU5UJykgeyB0aHJvdyBlOyB9XG4gIH1cbn1cblxuZXhwb3J0IGZ1bmN0aW9uIHJhbmRvbVN0cmluZygpIHtcbiAgLy8gQ3JhenlcbiAgcmV0dXJuIE1hdGgucmFuZG9tKCkudG9TdHJpbmcoMzYpLnJlcGxhY2UoL1teYS16MC05XSsvZywgJycpO1xufSJdfQ== \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.1/cli.integtest.js b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.1/cli.integtest.js new file mode 100644 index 000000000..a63578ecf --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.64.1/cli.integtest.js @@ -0,0 +1,599 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = require("fs"); +const os = require("os"); +const path = require("path"); +const aws_helpers_1 = require("./aws-helpers"); +const cdk_helpers_1 = require("./cdk-helpers"); +const test_helpers_1 = require("./test-helpers"); +jest.setTimeout(600 * 1000); +test_helpers_1.integTest('VPC Lookup', cdk_helpers_1.withDefaultFixture(async (fixture) => { + fixture.log('Making sure we are clean before starting.'); + await fixture.cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setting up: creating a VPC with known tags'); + await fixture.cdkDeploy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setup complete!'); + fixture.log('Verifying we can now import that VPC'); + await fixture.cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' } }); +})); +test_helpers_1.integTest('Two ways of shoing the version', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const version1 = await fixture.cdk(['version'], { verbose: false }); + const version2 = await fixture.cdk(['--version'], { verbose: false }); + expect(version1).toEqual(version2); +})); +test_helpers_1.integTest('Termination protection', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const stackName = 'termination-protection'; + await fixture.cdkDeploy(stackName); + // Try a destroy that should fail + await expect(fixture.cdkDestroy(stackName)).rejects.toThrow('exited with error'); + // Can update termination protection even though the change set doesn't contain changes + await fixture.cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); + await fixture.cdkDestroy(stackName); +})); +test_helpers_1.integTest('cdk synth', cdk_helpers_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdk(['synth', fixture.fullStackName('test-1')], { verbose: false })).resolves.toEqual(`Resources: + topic69831491: + Type: AWS::SNS::Topic + Metadata: + aws:cdk:path: ${fixture.stackNamePrefix}-test-1/topic/Resource`); + await expect(fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false })).resolves.toEqual(`Resources: + topic152D84A37: + Type: AWS::SNS::Topic + Metadata: + aws:cdk:path: ${fixture.stackNamePrefix}-test-2/topic1/Resource + topic2A4FB547F: + Type: AWS::SNS::Topic + Metadata: + aws:cdk:path: ${fixture.stackNamePrefix}-test-2/topic2/Resource`); +})); +test_helpers_1.integTest('ssm parameter provider error', cdk_helpers_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdk(['synth', + fixture.fullStackName('missing-ssm-parameter'), + '-c', 'test:ssm-parameter-name=/does/not/exist'], { + allowErrExit: true, + })).resolves.toContain('SSM parameter not available in account'); +})); +test_helpers_1.integTest('automatic ordering', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // Deploy the consuming stack which will include the producing stack + await fixture.cdkDeploy('order-consuming'); + // Destroy the providing stack which will include the consuming stack + await fixture.cdkDestroy('order-providing'); +})); +test_helpers_1.integTest('context setting', cdk_helpers_1.withDefaultFixture(async (fixture) => { + await fs_1.promises.writeFile(path.join(fixture.integTestDir, 'cdk.context.json'), JSON.stringify({ + contextkey: 'this is the context value', + })); + try { + await expect(fixture.cdk(['context'])).resolves.toContain('this is the context value'); + // Test that deleting the contextkey works + await fixture.cdk(['context', '--reset', 'contextkey']); + await expect(fixture.cdk(['context'])).resolves.not.toContain('this is the context value'); + // Test that forced delete of the context key does not throw + await fixture.cdk(['context', '-f', '--reset', 'contextkey']); + } + finally { + await fs_1.promises.unlink(path.join(fixture.integTestDir, 'cdk.context.json')); + } +})); +test_helpers_1.integTest('deploy', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('test-2', { captureStderr: false }); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(2); +})); +test_helpers_1.integTest('deploy all', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const arns = await fixture.cdkDeploy('test-*', { captureStderr: false }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(arns.split('\n').length).toEqual(2); +})); +test_helpers_1.integTest('nested stack with parameters', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances + // of this test to run in parallel, othewise they will attempt to create the same SNS topic. + const stackArn = await fixture.cdkDeploy('with-nested-stack-using-parameters', { + options: ['--parameters', `MyTopicParam=${fixture.stackNamePrefix}ThereIsNoSpoon`], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(1); +})); +test_helpers_1.integTest('deploy without execute', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--no-execute'], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); +})); +test_helpers_1.integTest('security related changes without a CLI are expected to fail', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // redirect /dev/null to stdin, which means there will not be tty attached + // since this stack includes security-related changes, the deployment should + // immediately fail because we can't confirm the changes + const stackName = 'iam-test'; + await expect(fixture.cdkDeploy(stackName, { + options: ['<', '/dev/null'], + neverRequireApproval: false, + })).rejects.toThrow('exited with error'); + // Ensure stack was not deployed + await expect(fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName(stackName), + })).rejects.toThrow('does not exist'); +})); +test_helpers_1.integTest('deploy wildcard with outputs', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs_1.promises.mkdir(path.dirname(outputsFile), { recursive: true }); + await fixture.cdkDeploy(['outputs-test-*'], { + options: ['--outputs-file', outputsFile], + }); + const outputs = JSON.parse((await fs_1.promises.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + expect(outputs).toEqual({ + [`${fixture.stackNamePrefix}-outputs-test-1`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-1MyTopic`, + }, + [`${fixture.stackNamePrefix}-outputs-test-2`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-2MyOtherTopic`, + }, + }); +})); +test_helpers_1.integTest('deploy with parameters', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}bazinga`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}bazinga`, + }, + ]); +})); +test_helpers_1.integTest('update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('param-test-1'), + }); + const stackArn = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackId; + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('ROLLBACK_COMPLETE'); + // WHEN + const newStackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + const newStackResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: newStackArn, + }); + // THEN + expect(stackArn).not.toEqual(newStackArn); // new stack was created + expect((_c = newStackResponse.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('CREATE_COMPLETE'); + expect((_d = newStackResponse.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('stack in UPDATE_ROLLBACK_COMPLETE state can be updated', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}nice`, + ], + captureStderr: false, + }); + let response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('CREATE_COMPLETE'); + // bad parameter name with @ will put stack into UPDATE_ROLLBACK_COMPLETE + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + ; + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('UPDATE_ROLLBACK_COMPLETE'); + // WHEN + await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + // THEN + expect((_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('UPDATE_COMPLETE'); + expect((_d = response.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('deploy with wildcard and parameters', cdk_helpers_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('param-test-*', { + options: [ + '--parameters', `${fixture.stackNamePrefix}-param-test-1:TopicNameParam=${fixture.stackNamePrefix}bazinga`, + '--parameters', `${fixture.stackNamePrefix}-param-test-2:OtherTopicNameParam=${fixture.stackNamePrefix}ThatsMySpot`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:DisplayNameParam=${fixture.stackNamePrefix}HeyThere`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:OtherDisplayNameParam=${fixture.stackNamePrefix}AnotherOne`, + ], + }); +})); +test_helpers_1.integTest('deploy with parameters multi', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + const paramVal1 = `${fixture.stackNamePrefix}bazinga`; + const paramVal2 = `${fixture.stackNamePrefix}=jagshemash`; + const stackArn = await fixture.cdkDeploy('param-test-3', { + options: [ + '--parameters', `DisplayNameParam=${paramVal1}`, + '--parameters', `OtherDisplayNameParam=${paramVal2}`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'DisplayNameParam', + ParameterValue: paramVal1, + }, + { + ParameterKey: 'OtherDisplayNameParam', + ParameterValue: paramVal2, + }, + ]); +})); +test_helpers_1.integTest('deploy with notification ARN', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a; + const topicName = `${fixture.stackNamePrefix}-test-topic`; + const response = await fixture.aws.sns('createTopic', { Name: topicName }); + const topicArn = response.TopicArn; + try { + await fixture.cdkDeploy('test-2', { + options: ['--notification-arns', topicArn], + }); + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('test-2'), + }); + expect((_a = describeResponse.Stacks) === null || _a === void 0 ? void 0 : _a[0].NotificationARNs).toEqual([topicArn]); + } + finally { + await fixture.aws.sns('deleteTopic', { + TopicArn: topicArn, + }); + } +})); +test_helpers_1.integTest('deploy with role', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const roleName = `${fixture.stackNamePrefix}-test-role`; + await deleteRole(); + const createResponse = await fixture.aws.iam('createRole', { + RoleName: roleName, + AssumeRolePolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: 'sts:AssumeRole', + Principal: { Service: 'cloudformation.amazonaws.com' }, + Effect: 'Allow', + }, { + Action: 'sts:AssumeRole', + Principal: { AWS: (await fixture.aws.sts('getCallerIdentity', {})).Arn }, + Effect: 'Allow', + }], + }), + }); + const roleArn = createResponse.Role.Arn; + try { + await fixture.aws.iam('putRolePolicy', { + RoleName: roleName, + PolicyName: 'DefaultPolicy', + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: '*', + Resource: '*', + Effect: 'Allow', + }], + }), + }); + await aws_helpers_1.retry(fixture.output, 'Trying to assume fresh role', aws_helpers_1.retry.forSeconds(300), async () => { + await fixture.aws.sts('assumeRole', { + RoleArn: roleArn, + RoleSessionName: 'testing', + }); + }); + // In principle, the role has replicated from 'us-east-1' to wherever we're testing. + // Give it a little more sleep to make sure CloudFormation is not hitting a box + // that doesn't have it yet. + await aws_helpers_1.sleep(5000); + await fixture.cdkDeploy('test-2', { + options: ['--role-arn', roleArn], + }); + // Immediately delete the stack again before we delete the role. + // + // Since roles are sticky, if we delete the role before the stack, subsequent DeleteStack + // operations will fail when CloudFormation tries to assume the role that's already gone. + await fixture.cdkDestroy('test-2'); + } + finally { + await deleteRole(); + } + async function deleteRole() { + try { + for (const policyName of (await fixture.aws.iam('listRolePolicies', { RoleName: roleName })).PolicyNames) { + await fixture.aws.iam('deleteRolePolicy', { + RoleName: roleName, + PolicyName: policyName, + }); + } + await fixture.aws.iam('deleteRole', { RoleName: roleName }); + } + catch (e) { + if (e.message.indexOf('cannot be found') > -1) { + return; + } + throw e; + } + } +})); +test_helpers_1.integTest('cdk diff', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // We can make it fail by passing --fail + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1')])) + .rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail on multiple stacks exits with error if any of the stacks contains a diff', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // GIVEN + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + await fixture.cdkDeploy('test-2'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('There were no differences'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail with multiple stack exits with if any of the stacks contains a diff', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('test-1'); + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('There were no differences'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('deploy stack with docker asset', cdk_helpers_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('docker'); +})); +test_helpers_1.integTest('deploy and test stack with lambda asset', cdk_helpers_1.withDefaultFixture(async (fixture) => { + var _a, _b; + const stackArn = await fixture.cdkDeploy('lambda', { captureStderr: false }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + const lambdaArn = (_b = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Outputs) === null || _b === void 0 ? void 0 : _b[0].OutputValue; + if (lambdaArn === undefined) { + throw new Error('Stack did not have expected Lambda ARN output'); + } + const output = await fixture.aws.lambda('invoke', { + FunctionName: lambdaArn, + }); + expect(JSON.stringify(output.Payload)).toContain('dear asset'); +})); +test_helpers_1.integTest('cdk ls', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls'], { captureStderr: false }); + const expectedStacks = [ + 'conditional-resource', + 'docker', + 'docker-with-custom-file', + 'failed', + 'iam-test', + 'lambda', + 'missing-ssm-parameter', + 'order-providing', + 'outputs-test-1', + 'outputs-test-2', + 'param-test-1', + 'param-test-2', + 'param-test-3', + 'termination-protection', + 'test-1', + 'test-2', + 'with-nested-stack', + 'with-nested-stack-using-parameters', + 'order-consuming', + ]; + for (const stack of expectedStacks) { + expect(listing).toContain(fixture.fullStackName(stack)); + } +})); +test_helpers_1.integTest('deploy stack without resource', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // Deploy the stack without resources + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + // This should have succeeded but not deployed the stack. + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); + // Deploy the stack with resources + await fixture.cdkDeploy('conditional-resource'); + // Then again WITHOUT resources (this should destroy the stack) + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); +})); +test_helpers_1.integTest('IAM diff', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]); + // Roughly check for a table like this: + // + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ + // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ + expect(output).toContain('${SomeRole.Arn}'); + expect(output).toContain('sts:AssumeRole'); + expect(output).toContain('ec2.amazonaws.com'); +})); +test_helpers_1.integTest('fast deploy', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // we are using a stack with a nested stack because CFN will always attempt to + // update a nested stack, which will allow us to verify that updates are actually + // skipped unless --force is specified. + const stackArn = await fixture.cdkDeploy('with-nested-stack', { captureStderr: false }); + const changeSet1 = await getLatestChangeSet(); + // Deploy the same stack again, there should be no new change set created + await fixture.cdkDeploy('with-nested-stack'); + const changeSet2 = await getLatestChangeSet(); + expect(changeSet2.ChangeSetId).toEqual(changeSet1.ChangeSetId); + // Deploy the stack again with --force, now we should create a changeset + await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] }); + const changeSet3 = await getLatestChangeSet(); + expect(changeSet3.ChangeSetId).not.toEqual(changeSet2.ChangeSetId); + // Deploy the stack again with tags, expected to create a new changeset + // even though the resources didn't change. + await fixture.cdkDeploy('with-nested-stack', { options: ['--tags', 'key=value'] }); + const changeSet4 = await getLatestChangeSet(); + expect(changeSet4.ChangeSetId).not.toEqual(changeSet3.ChangeSetId); + async function getLatestChangeSet() { + var _a, _b, _c; + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn }); + if (!((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0])) { + throw new Error('Did not get a ChangeSet at all'); + } + fixture.log(`Found Change Set ${(_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].ChangeSetId}`); + return (_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0]; + } +})); +test_helpers_1.integTest('failed deploy does not hang', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. + await expect(fixture.cdkDeploy('failed')).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('can still load old assemblies', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx'); + const testAssembliesDirectory = path.join(__dirname, 'cloud-assemblies'); + for (const asmdir of await listChildDirs(testAssembliesDirectory)) { + fixture.log(`ASSEMBLY ${asmdir}`); + await cdk_helpers_1.cloneDirectory(asmdir, cxAsmDir); + // Some files in the asm directory that have a .js extension are + // actually treated as templates. Evaluate them using NodeJS. + const templates = await listChildren(cxAsmDir, fullPath => Promise.resolve(fullPath.endsWith('.js'))); + for (const template of templates) { + const targetName = template.replace(/.js$/, ''); + await cdk_helpers_1.shell([process.execPath, template, '>', targetName], { + cwd: cxAsmDir, + output: fixture.output, + modEnv: { + TEST_ACCOUNT: await fixture.aws.account(), + TEST_REGION: fixture.aws.region, + }, + }); + } + // Use this directory as a Cloud Assembly + const output = await fixture.cdk([ + '--app', cxAsmDir, + '-v', + 'synth', + ]); + // Assert that there was no providerError in CDK's stderr + // Because we rely on the app/framework to actually error in case the + // provider fails, we inspect the logs here. + expect(output).not.toContain('$providerError'); + } +})); +test_helpers_1.integTest('generating and loading assembly', cdk_helpers_1.withDefaultFixture(async (fixture) => { + const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`; + await fixture.shell(['rm', '-rf', asmOutputDir]); + // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory. + await fixture.cdk(['synth']); + await fixture.cdk(['synth', '--output', asmOutputDir]); + // cdk.out in the current directory and the indicated --output should be the same + await fixture.shell(['diff', 'cdk.out', asmOutputDir]); + // Check that we can 'ls' the synthesized asm. + // Change to some random directory to make sure we're not accidentally loading cdk.json + const list = await fixture.cdk(['--app', asmOutputDir, 'ls'], { cwd: os.tmpdir() }); + // Same stacks we know are in the app + expect(list).toContain(`${fixture.stackNamePrefix}-lambda`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-1`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-2`); + // Check that we can use '.' and just synth ,the generated asm + const stackTemplate = await fixture.cdk(['--app', '.', 'synth', fixture.fullStackName('test-2')], { + cwd: asmOutputDir, + }); + expect(stackTemplate).toContain('topic152D84A37'); + // Deploy a Lambda from the copied asm + await fixture.cdkDeploy('lambda', { options: ['-a', '.'], cwd: asmOutputDir }); + // Remove (rename) the original custom docker file that was used during synth. + // this verifies that the assemly has a copy of it and that the manifest uses + // relative paths to reference to it. + const customDockerFile = path.join(fixture.integTestDir, 'docker', 'Dockerfile.Custom'); + await fs_1.promises.rename(customDockerFile, `${customDockerFile}~`); + try { + // deploy a docker image with custom file without synth (uses assets) + await fixture.cdkDeploy('docker-with-custom-file', { options: ['-a', '.'], cwd: asmOutputDir }); + } + finally { + // Rename back to restore fixture to original state + await fs_1.promises.rename(`${customDockerFile}~`, customDockerFile); + } +})); +test_helpers_1.integTest('templates on disk contain metadata resource, also in nested assemblies', cdk_helpers_1.withDefaultFixture(async (fixture) => { + // Synth first, and switch on version reporting because cdk.json is disabling it + await fixture.cdk(['synth', '--version-reporting=true']); + // Load template from disk from root assembly + const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']); + expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); + // Load template from nested assembly + const nestedTemplateContents = await fixture.shell(['cat', 'cdk.out/assembly-*-stage/*-stage-StackInStage.template.json']); + expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); +})); +async function listChildren(parent, pred) { + const ret = new Array(); + for (const child of await fs_1.promises.readdir(parent, { encoding: 'utf-8' })) { + const fullPath = path.join(parent, child.toString()); + if (await pred(fullPath)) { + ret.push(fullPath); + } + } + return ret; +} +async function listChildDirs(parent) { + return listChildren(parent, async (fullPath) => (await fs_1.promises.stat(fullPath)).isDirectory()); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLmludGVndGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNsaS5pbnRlZ3Rlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSwyQkFBb0M7QUFDcEMseUJBQXlCO0FBQ3pCLDZCQUE2QjtBQUM3QiwrQ0FBNkM7QUFDN0MsK0NBQTBFO0FBQzFFLGlEQUEyQztBQUUzQyxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsR0FBRyxJQUFJLENBQUMsQ0FBQztBQUU1Qix3QkFBUyxDQUFDLFlBQVksRUFBRSxnQ0FBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDM0QsT0FBTyxDQUFDLEdBQUcsQ0FBQywyQ0FBMkMsQ0FBQyxDQUFDO0lBQ3pELE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFFckYsT0FBTyxDQUFDLEdBQUcsQ0FBQyw0Q0FBNEMsQ0FBQyxDQUFDO0lBQzFELE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDcEYsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRS9CLE9BQU8sQ0FBQyxHQUFHLENBQUMsc0NBQXNDLENBQUMsQ0FBQztJQUNwRCxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsWUFBWSxFQUFFLEVBQUUsTUFBTSxFQUFFLEVBQUUsa0JBQWtCLEVBQUUsUUFBUSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBQ3RGLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGdDQUFnQyxFQUFFLGdDQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUMvRSxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFH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LENBQUMsQ0FBQyxDQUFDO0lBRTNILE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLHNCQUFzQixDQUFDLENBQUMsU0FBUyxDQUFDLFdBQVcsQ0FBQyxDQUFDLFVBQVUsRUFBRSxDQUFDO0FBQ2hGLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSixLQUFLLFVBQVUsWUFBWSxDQUFDLE1BQWMsRUFBRSxJQUFxQztJQUMvRSxNQUFNLEdBQUcsR0FBRyxJQUFJLEtBQUssRUFBVSxDQUFDO0lBQ2hDLEtBQUssTUFBTSxLQUFLLElBQUksTUFBTSxhQUFFLENBQUMsT0FBTyxDQUFDLE1BQU0sRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxFQUFFO1FBQ25FLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsTUFBTSxFQUFFLEtBQUssQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO1FBQ3JELElBQUksTUFBTSxJQUFJLENBQUMsUUFBUSxDQUFDLEVBQUU7WUFDeEIsR0FBRyxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQztTQUNwQjtLQUNGO0lBQ0QsT0FBTyxHQUFHLENBQUM7QUFDYixDQUFDO0FBRUQsS0FBSyxVQUFVLGFBQWEsQ0FBQyxNQUFjO0lBQ3pDLE9BQU8sWUFBWSxDQUFDLE1BQU0sRUFBRSxLQUFLLEVBQUUsUUFBZ0IsRUFBRSxFQUFFLENBQUMsQ0FBQyxNQUFNLGFBQUUsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxXQUFXLEVBQUUsQ0FBQyxDQUFDO0FBQ25HLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBwcm9taXNlcyBhcyBmcyB9IGZyb20gJ2ZzJztcbmltcG9ydCAqIGFzIG9zIGZyb20gJ29zJztcbmltcG9ydCAqIGFzIHBhdGggZnJvbSAncGF0aCc7XG5pbXBvcnQgeyByZXRyeSwgc2xlZXAgfSBmcm9tICcuL2F3cy1oZWxwZXJzJztcbmltcG9ydCB7IGNsb25lRGlyZWN0b3J5LCBzaGVsbCwgd2l0aERlZmF1bHRGaXh0dXJlIH0gZnJvbSAnLi9jZGstaGVscGVycyc7XG5pbXBvcnQgeyBpbnRlZ1Rlc3QgfSBmcm9tICcuL3Rlc3QtaGVscGVycyc7XG5cbmplc3Quc2V0VGltZW91dCg2MDAgKiAxMDAwKTtcblxuaW50ZWdUZXN0KCdWUEMgTG9va3VwJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGZpeHR1cmUubG9nKCdNYWtpbmcgc3VyZSB3ZSBhcmUgY2xlYW4gYmVmb3JlIHN0YXJ0aW5nLicpO1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koJ2RlZmluZS12cGMnLCB7IG1vZEVudjogeyBFTkFCTEVfVlBDX1RFU1RJTkc6ICdERUZJTkUnIH0gfSk7XG5cbiAgZml4dHVyZS5sb2coJ1NldHRpbmcgdXA6IGNyZWF0aW5nIGEgVlBDIHdpdGgga25vd24gdGFncycpO1xuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZGVmaW5lLXZwYycsIHsgbW9kRW52OiB7IEVOQUJMRV9WUENfVEVTVElORzogJ0RFRklORScgfSB9KTtcbiAgZml4dHVyZS5sb2coJ1NldHVwIGNvbXBsZXRlIScpO1xuXG4gIGZpeHR1cmUubG9nKCdWZXJpZnlpbmcgd2UgY2FuIG5vdyBpbXBvcnQgdGhhdCBWUEMnKTtcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ2ltcG9ydC12cGMnLCB7IG1vZEVudjogeyBFTkFCTEVfVlBDX1RFU1RJTkc6ICdJTVBPUlQnIH0gfSk7XG59KSk7XG5cbmludGVnVGVzdCgnVHdvIHdheXMgb2Ygc2hvaW5nIHRoZSB2ZXJzaW9uJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHZlcnNpb24xID0gYXdhaXQgZml4dHVyZS5jZGsoWyd2ZXJzaW9uJ10sIHsgdmVyYm9zZTogZmFsc2UgfSk7XG4gIGNvbnN0IHZlcnNpb24yID0gYXdhaXQgZml4dHVyZS5jZGsoWyctLXZlcnNpb24nXSwgeyB2ZXJib3NlOiBmYWxzZSB9KTtcblxuICBleHBlY3QodmVyc2lvbjEpLnRvRXF1YWwodmVyc2lvbjIpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ1Rlcm1pbmF0aW9uIHByb3RlY3Rpb24nLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgc3RhY2tOYW1lID0gJ3Rlcm1pbmF0aW9uLXByb3RlY3Rpb24nO1xuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveShzdGFja05hbWUpO1xuXG4gIC8vIFRyeSBhIGRlc3Ryb3kgdGhhdCBzaG91bGQgZmFpbFxuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGtEZXN0cm95KHN0YWNrTmFtZSkpLnJlamVjdHMudG9UaHJvdygnZXhpdGVkIHdpdGggZXJyb3InKTtcblxuICAvLyBDYW4gdXBkYXRlIHRlcm1pbmF0aW9uIHByb3RlY3Rpb24gZXZlbiB0aG91Z2ggdGhlIGNoYW5nZSBzZXQgZG9lc24ndCBjb250YWluIGNoYW5nZXNcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lLCB7IG1vZEVudjogeyBURVJNSU5BVElPTl9QUk9URUNUSU9OOiAnRkFMU0UnIH0gfSk7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVzdHJveShzdGFja05hbWUpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NkayBzeW50aCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGsoWydzeW50aCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldLCB7IHZlcmJvc2U6IGZhbHNlIH0pKS5yZXNvbHZlcy50b0VxdWFsKFxuICAgIGBSZXNvdXJjZXM6XG4gIHRvcGljNjk4MzE0OTE6XG4gICAgVHlwZTogQVdTOjpTTlM6OlRvcGljXG4gICAgTWV0YWRhdGE6XG4gICAgICBhd3M6Y2RrOnBhdGg6ICR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMS90b3BpYy9SZXNvdXJjZWApO1xuXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ3N5bnRoJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0sIHsgdmVyYm9zZTogZmFsc2UgfSkpLnJlc29sdmVzLnRvRXF1YWwoXG4gICAgYFJlc291cmNlczpcbiAgdG9waWMxNTJEODRBMzc6XG4gICAgVHlwZTogQVdTOjpTTlM6OlRvcGljXG4gICAgTWV0YWRhdGE6XG4gICAgICBhd3M6Y2RrOnBhdGg6ICR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMi90b3BpYzEvUmVzb3VyY2VcbiAgdG9waWMyQTRGQjU0N0Y6XG4gICAgVHlwZTogQVdTOjpTTlM6OlRvcGljXG4gICAgTWV0YWRhdGE6XG4gICAgICBhd3M6Y2RrOnBhdGg6ICR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMi90b3BpYzIvUmVzb3VyY2VgKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzc20gcGFyYW1ldGVyIHByb3ZpZGVyIGVycm9yJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ3N5bnRoJyxcbiAgICBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ21pc3Npbmctc3NtLXBhcmFtZXRlcicpLFxuICAgICctYycsICd0ZXN0OnNzbS1wYXJhbWV0ZXItbmFtZT0vZG9lcy9ub3QvZXhpc3QnXSwge1xuICAgIGFsbG93RXJyRXhpdDogdHJ1ZSxcbiAgfSkpLnJlc29sdmVzLnRvQ29udGFpbignU1NNIHBhcmFtZXRlciBub3QgYXZhaWxhYmxlIGluIGFjY291bnQnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdhdXRvbWF0aWMgb3JkZXJpbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gRGVwbG95IHRoZSBjb25zdW1pbmcgc3RhY2sgd2hpY2ggd2lsbCBpbmNsdWRlIHRoZSBwcm9kdWNpbmcgc3RhY2tcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ29yZGVyLWNvbnN1bWluZycpO1xuXG4gIC8vIERlc3Ryb3kgdGhlIHByb3ZpZGluZyBzdGFjayB3aGljaCB3aWxsIGluY2x1ZGUgdGhlIGNvbnN1bWluZyBzdGFja1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koJ29yZGVyLXByb3ZpZGluZycpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NvbnRleHQgc2V0dGluZycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBmcy53cml0ZUZpbGUocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpLCBKU09OLnN0cmluZ2lmeSh7XG4gICAgY29udGV4dGtleTogJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnLFxuICB9KSk7XG4gIHRyeSB7XG4gICAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrKFsnY29udGV4dCddKSkucmVzb2x2ZXMudG9Db250YWluKCd0aGlzIGlzIHRoZSBjb250ZXh0IHZhbHVlJyk7XG5cbiAgICAvLyBUZXN0IHRoYXQgZGVsZXRpbmcgdGhlIGNvbnRleHRrZXkgd29ya3NcbiAgICBhd2FpdCBmaXh0dXJlLmNkayhbJ2NvbnRleHQnLCAnLS1yZXNldCcsICdjb250ZXh0a2V5J10pO1xuICAgIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2NvbnRleHQnXSkpLnJlc29sdmVzLm5vdC50b0NvbnRhaW4oJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnKTtcblxuICAgIC8vIFRlc3QgdGhhdCBmb3JjZWQgZGVsZXRlIG9mIHRoZSBjb250ZXh0IGtleSBkb2VzIG5vdCB0aHJvd1xuICAgIGF3YWl0IGZpeHR1cmUuY2RrKFsnY29udGV4dCcsICctZicsICctLXJlc2V0JywgJ2NvbnRleHRrZXknXSk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmcy51bmxpbmsocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCd0ZXN0LTInLCB7IGNhcHR1cmVTdGRlcnI6IGZhbHNlIH0pO1xuXG4gIC8vIHZlcmlmeSB0aGUgbnVtYmVyIG9mIHJlc291cmNlcyBpbiB0aGUgc3RhY2tcbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja1Jlc291cmNlcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrUmVzb3VyY2VzPy5sZW5ndGgpLnRvRXF1YWwoMik7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IGFsbCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBhcm5zID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtKicsIHsgY2FwdHVyZVN0ZGVycjogZmFsc2UgfSk7XG5cbiAgLy8gdmVyaWZ5IHRoYXQgd2Ugb25seSBkZXBsb3llZCBhIHNpbmdsZSBzdGFjayAodGhlcmUncyBhIHNpbmdsZSBBUk4gaW4gdGhlIG91dHB1dClcbiAgZXhwZWN0KGFybnMuc3BsaXQoJ1xcbicpLmxlbmd0aCkudG9FcXVhbCgyKTtcbn0pKTtcblxuaW50ZWdUZXN0KCduZXN0ZWQgc3RhY2sgd2l0aCBwYXJhbWV0ZXJzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIC8vIFNUQUNLX05BTUVfUFJFRklYIGlzIHVzZWQgaW4gTXlUb3BpY1BhcmFtIHRvIGFsbG93IG11bHRpcGxlIGluc3RhbmNlc1xuICAvLyBvZiB0aGlzIHRlc3QgdG8gcnVuIGluIHBhcmFsbGVsLCBvdGhld2lzZSB0aGV5IHdpbGwgYXR0ZW1wdCB0byBjcmVhdGUgdGhlIHNhbWUgU05TIHRvcGljLlxuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCd3aXRoLW5lc3RlZC1zdGFjay11c2luZy1wYXJhbWV0ZXJzJywge1xuICAgIG9wdGlvbnM6IFsnLS1wYXJhbWV0ZXJzJywgYE15VG9waWNQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fVRoZXJlSXNOb1Nwb29uYF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIC8vIHZlcmlmeSB0aGF0IHdlIG9ubHkgZGVwbG95ZWQgYSBzaW5nbGUgc3RhY2sgKHRoZXJlJ3MgYSBzaW5nbGUgQVJOIGluIHRoZSBvdXRwdXQpXG4gIGV4cGVjdChzdGFja0Fybi5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDEpO1xuXG4gIC8vIHZlcmlmeSB0aGUgbnVtYmVyIG9mIHJlc291cmNlcyBpbiB0aGUgc3RhY2tcbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja1Jlc291cmNlcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrUmVzb3VyY2VzPy5sZW5ndGgpLnRvRXF1YWwoMSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGhvdXQgZXhlY3V0ZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCd0ZXN0LTInLCB7XG4gICAgb3B0aW9uczogWyctLW5vLWV4ZWN1dGUnXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG4gIC8vIHZlcmlmeSB0aGF0IHdlIG9ubHkgZGVwbG95ZWQgYSBzaW5nbGUgc3RhY2sgKHRoZXJlJ3MgYSBzaW5nbGUgQVJOIGluIHRoZSBvdXRwdXQpXG4gIGV4cGVjdChzdGFja0Fybi5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDEpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuXG4gIGV4cGVjdChyZXNwb25zZS5TdGFja3M/LlswXS5TdGFja1N0YXR1cykudG9FcXVhbCgnUkVWSUVXX0lOX1BST0dSRVNTJyk7XG59KSk7XG5cbmludGVnVGVzdCgnc2VjdXJpdHkgcmVsYXRlZCBjaGFuZ2VzIHdpdGhvdXQgYSBDTEkgYXJlIGV4cGVjdGVkIHRvIGZhaWwnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gcmVkaXJlY3QgL2Rldi9udWxsIHRvIHN0ZGluLCB3aGljaCBtZWFucyB0aGVyZSB3aWxsIG5vdCBiZSB0dHkgYXR0YWNoZWRcbiAgLy8gc2luY2UgdGhpcyBzdGFjayBpbmNsdWRlcyBzZWN1cml0eS1yZWxhdGVkIGNoYW5nZXMsIHRoZSBkZXBsb3ltZW50IHNob3VsZFxuICAvLyBpbW1lZGlhdGVseSBmYWlsIGJlY2F1c2Ugd2UgY2FuJ3QgY29uZmlybSB0aGUgY2hhbmdlc1xuICBjb25zdCBzdGFja05hbWUgPSAnaWFtLXRlc3QnO1xuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lLCB7XG4gICAgb3B0aW9uczogWyc8JywgJy9kZXYvbnVsbCddLCAvLyBINHgsIHRoaXMgb25seSB3b3JrcyBiZWNhdXNlIEkgaGFwcGVuIHRvIGtub3cgd2UgcGFzcyBzaGVsbDogdHJ1ZS5cbiAgICBuZXZlclJlcXVpcmVBcHByb3ZhbDogZmFsc2UsXG4gIH0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG5cbiAgLy8gRW5zdXJlIHN0YWNrIHdhcyBub3QgZGVwbG95ZWRcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFja05hbWUpLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdkb2VzIG5vdCBleGlzdCcpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aWxkY2FyZCB3aXRoIG91dHB1dHMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgb3V0cHV0c0ZpbGUgPSBwYXRoLmpvaW4oZml4dHVyZS5pbnRlZ1Rlc3REaXIsICdvdXRwdXRzJywgJ291dHB1dHMuanNvbicpO1xuICBhd2FpdCBmcy5ta2RpcihwYXRoLmRpcm5hbWUob3V0cHV0c0ZpbGUpLCB7IHJlY3Vyc2l2ZTogdHJ1ZSB9KTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveShbJ291dHB1dHMtdGVzdC0qJ10sIHtcbiAgICBvcHRpb25zOiBbJy0tb3V0cHV0cy1maWxlJywgb3V0cHV0c0ZpbGVdLFxuICB9KTtcblxuICBjb25zdCBvdXRwdXRzID0gSlNPTi5wYXJzZSgoYXdhaXQgZnMucmVhZEZpbGUob3V0cHV0c0ZpbGUsIHsgZW5jb2Rpbmc6ICd1dGYtOCcgfSkpLnRvU3RyaW5nKCkpO1xuICBleHBlY3Qob3V0cHV0cykudG9FcXVhbCh7XG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMWBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMU15VG9waWNgLFxuICAgIH0sXG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMmBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMk15T3RoZXJUb3BpY2AsXG4gICAgfSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWJhemluZ2FgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuXG4gIGV4cGVjdChyZXNwb25zZS5TdGFja3M/LlswXS5QYXJhbWV0ZXJzKS50b0VxdWFsKFtcbiAgICB7XG4gICAgICBQYXJhbWV0ZXJLZXk6ICdUb3BpY05hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YmF6aW5nYWAsXG4gICAgfSxcbiAgXSk7XG59KSk7XG5cbmludGVnVGVzdCgndXBkYXRlIHRvIHN0YWNrIGluIFJPTExCQUNLX0NPTVBMRVRFIHN0YXRlIHdpbGwgZGVsZXRlIHN0YWNrIGFuZCBjcmVhdGUgYSBuZXcgb25lJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIC8vIEdJVkVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogZml4dHVyZS5mdWxsU3RhY2tOYW1lKCdwYXJhbS10ZXN0LTEnKSxcbiAgfSk7XG5cbiAgY29uc3Qgc3RhY2tBcm4gPSByZXNwb25zZS5TdGFja3M/LlswXS5TdGFja0lkO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1JPTExCQUNLX0NPTVBMRVRFJyk7XG5cbiAgLy8gV0hFTlxuICBjb25zdCBuZXdTdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IG5ld1N0YWNrUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBuZXdTdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QgKHN0YWNrQXJuKS5ub3QudG9FcXVhbChuZXdTdGFja0Fybik7IC8vIG5ldyBzdGFjayB3YXMgY3JlYXRlZFxuICBleHBlY3QobmV3U3RhY2tSZXNwb25zZS5TdGFja3M/LlswXS5TdGFja1N0YXR1cykudG9FcXVhbCgnQ1JFQVRFX0NPTVBMRVRFJyk7XG4gIGV4cGVjdChuZXdTdGFja1Jlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ1RvcGljTmFtZVBhcmFtJyxcbiAgICAgIFBhcmFtZXRlclZhbHVlOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1hbGxnb29kYCxcbiAgICB9LFxuICBdKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzdGFjayBpbiBVUERBVEVfUk9MTEJBQ0tfQ09NUExFVEUgc3RhdGUgY2FuIGJlIHVwZGF0ZWQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1uaWNlYCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KTtcblxuICBsZXQgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlN0YWNrU3RhdHVzKS50b0VxdWFsKCdDUkVBVEVfQ09NUExFVEUnKTtcblxuICAvLyBiYWQgcGFyYW1ldGVyIG5hbWUgd2l0aCBAIHdpbGwgcHV0IHN0YWNrIGludG8gVVBEQVRFX1JPTExCQUNLX0NPTVBMRVRFXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpOztcblxuICByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcblxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9ST0xMQkFDS19DT01QTEVURScpO1xuXG4gIC8vIFdIRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYFRvcGljTmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YWxsZ29vZGAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9DT01QTEVURScpO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uUGFyYW1ldGVycykudG9FcXVhbChbXG4gICAge1xuICAgICAgUGFyYW1ldGVyS2V5OiAnVG9waWNOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIHdpbGRjYXJkIGFuZCBwYXJhbWV0ZXJzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LSonLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTE6VG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tcGFyYW0tdGVzdC0yOk90aGVyVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1UaGF0c015U3BvdGAsXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXBhcmFtLXRlc3QtMzpEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9SGV5VGhlcmVgLFxuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTM6T3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9QW5vdGhlck9uZWAsXG4gICAgXSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycyBtdWx0aScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBwYXJhbVZhbDEgPSBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYDtcbiAgY29uc3QgcGFyYW1WYWwyID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9PWphZ3NoZW1hc2hgO1xuXG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMycsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYERpc3BsYXlOYW1lUGFyYW09JHtwYXJhbVZhbDF9YCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgT3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7cGFyYW1WYWwyfWAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ0Rpc3BsYXlOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IHBhcmFtVmFsMSxcbiAgICB9LFxuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ090aGVyRGlzcGxheU5hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogcGFyYW1WYWwyLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIG5vdGlmaWNhdGlvbiBBUk4nLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgdG9waWNOYW1lID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtdG9waWNgO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3Muc25zKCdjcmVhdGVUb3BpYycsIHsgTmFtZTogdG9waWNOYW1lIH0pO1xuICBjb25zdCB0b3BpY0FybiA9IHJlc3BvbnNlLlRvcGljQXJuITtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJywge1xuICAgICAgb3B0aW9uczogWyctLW5vdGlmaWNhdGlvbi1hcm5zJywgdG9waWNBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gdmVyaWZ5IHRoYXQgdGhlIHN0YWNrIHdlIGRlcGxveWVkIGhhcyBvdXIgbm90aWZpY2F0aW9uIEFSTlxuICAgIGNvbnN0IGRlc2NyaWJlUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyksXG4gICAgfSk7XG4gICAgZXhwZWN0KGRlc2NyaWJlUmVzcG9uc2UuU3RhY2tzPy5bMF0uTm90aWZpY2F0aW9uQVJOcykudG9FcXVhbChbdG9waWNBcm5dKTtcbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5zbnMoJ2RlbGV0ZVRvcGljJywge1xuICAgICAgVG9waWNBcm46IHRvcGljQXJuLFxuICAgIH0pO1xuICB9XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcm9sZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCByb2xlTmFtZSA9IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS10ZXN0LXJvbGVgO1xuXG4gIGF3YWl0IGRlbGV0ZVJvbGUoKTtcblxuICBjb25zdCBjcmVhdGVSZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnY3JlYXRlUm9sZScsIHtcbiAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgQXNzdW1lUm9sZVBvbGljeURvY3VtZW50OiBKU09OLnN0cmluZ2lmeSh7XG4gICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICBTdGF0ZW1lbnQ6IFt7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IFNlcnZpY2U6ICdjbG91ZGZvcm1hdGlvbi5hbWF6b25hd3MuY29tJyB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9LCB7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IEFXUzogKGF3YWl0IGZpeHR1cmUuYXdzLnN0cygnZ2V0Q2FsbGVySWRlbnRpdHknLCB7fSkpLkFybiB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9XSxcbiAgICB9KSxcbiAgfSk7XG4gIGNvbnN0IHJvbGVBcm4gPSBjcmVhdGVSZXNwb25zZS5Sb2xlLkFybjtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ3B1dFJvbGVQb2xpY3knLCB7XG4gICAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgICBQb2xpY3lOYW1lOiAnRGVmYXVsdFBvbGljeScsXG4gICAgICBQb2xpY3lEb2N1bWVudDogSlNPTi5zdHJpbmdpZnkoe1xuICAgICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICAgIFN0YXRlbWVudDogW3tcbiAgICAgICAgICBBY3Rpb246ICcqJyxcbiAgICAgICAgICBSZXNvdXJjZTogJyonLFxuICAgICAgICAgIEVmZmVjdDogJ0FsbG93JyxcbiAgICAgICAgfV0sXG4gICAgICB9KSxcbiAgICB9KTtcblxuICAgIGF3YWl0IHJldHJ5KGZpeHR1cmUub3V0cHV0LCAnVHJ5aW5nIHRvIGFzc3VtZSBmcmVzaCByb2xlJywgcmV0cnkuZm9yU2Vjb25kcygzMDApLCBhc3luYyAoKSA9PiB7XG4gICAgICBhd2FpdCBmaXh0dXJlLmF3cy5zdHMoJ2Fzc3VtZVJvbGUnLCB7XG4gICAgICAgIFJvbGVBcm46IHJvbGVBcm4sXG4gICAgICAgIFJvbGVTZXNzaW9uTmFtZTogJ3Rlc3RpbmcnLFxuICAgICAgfSk7XG4gICAgfSk7XG5cbiAgICAvLyBJbiBwcmluY2lwbGUsIHRoZSByb2xlIGhhcyByZXBsaWNhdGVkIGZyb20gJ3VzLWVhc3QtMScgdG8gd2hlcmV2ZXIgd2UncmUgdGVzdGluZy5cbiAgICAvLyBHaXZlIGl0IGEgbGl0dGxlIG1vcmUgc2xlZXAgdG8gbWFrZSBzdXJlIENsb3VkRm9ybWF0aW9uIGlzIG5vdCBoaXR0aW5nIGEgYm94XG4gICAgLy8gdGhhdCBkb2Vzbid0IGhhdmUgaXQgeWV0LlxuICAgIGF3YWl0IHNsZWVwKDUwMDApO1xuXG4gICAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMicsIHtcbiAgICAgIG9wdGlvbnM6IFsnLS1yb2xlLWFybicsIHJvbGVBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gSW1tZWRpYXRlbHkgZGVsZXRlIHRoZSBzdGFjayBhZ2FpbiBiZWZvcmUgd2UgZGVsZXRlIHRoZSByb2xlLlxuICAgIC8vXG4gICAgLy8gU2luY2Ugcm9sZXMgYXJlIHN0aWNreSwgaWYgd2UgZGVsZXRlIHRoZSByb2xlIGJlZm9yZSB0aGUgc3RhY2ssIHN1YnNlcXVlbnQgRGVsZXRlU3RhY2tcbiAgICAvLyBvcGVyYXRpb25zIHdpbGwgZmFpbCB3aGVuIENsb3VkRm9ybWF0aW9uIHRyaWVzIHRvIGFzc3VtZSB0aGUgcm9sZSB0aGF0J3MgYWxyZWFkeSBnb25lLlxuICAgIGF3YWl0IGZpeHR1cmUuY2RrRGVzdHJveSgndGVzdC0yJyk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBkZWxldGVSb2xlKCk7XG4gIH1cblxuICBhc3luYyBmdW5jdGlvbiBkZWxldGVSb2xlKCkge1xuICAgIHRyeSB7XG4gICAgICBmb3IgKGNvbnN0IHBvbGljeU5hbWUgb2YgKGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnbGlzdFJvbGVQb2xpY2llcycsIHsgUm9sZU5hbWU6IHJvbGVOYW1lIH0pKS5Qb2xpY3lOYW1lcykge1xuICAgICAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ2RlbGV0ZVJvbGVQb2xpY3knLCB7XG4gICAgICAgICAgUm9sZU5hbWU6IHJvbGVOYW1lLFxuICAgICAgICAgIFBvbGljeU5hbWU6IHBvbGljeU5hbWUsXG4gICAgICAgIH0pO1xuICAgICAgfVxuICAgICAgYXdhaXQgZml4dHVyZS5hd3MuaWFtKCdkZWxldGVSb2xlJywgeyBSb2xlTmFtZTogcm9sZU5hbWUgfSk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgaWYgKGUubWVzc2FnZS5pbmRleE9mKCdjYW5ub3QgYmUgZm91bmQnKSA+IC0xKSB7IHJldHVybjsgfVxuICAgICAgdGhyb3cgZTtcbiAgICB9XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignQVdTOjpTTlM6OlRvcGljJyk7XG5cbiAgLy8gV2UgY2FuIG1ha2UgaXQgZmFpbCBieSBwYXNzaW5nIC0tZmFpbFxuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGsoWydkaWZmJywgJy0tZmFpbCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKSlcbiAgICAucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NkayBkaWZmIC0tZmFpbCBvbiBtdWx0aXBsZSBzdGFja3MgZXhpdHMgd2l0aCBlcnJvciBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3QgZGlmZjEgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMScpXSk7XG4gIGV4cGVjdChkaWZmMSkudG9Db250YWluKCdBV1M6OlNOUzo6VG9waWMnKTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJyk7XG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignVGhlcmUgd2VyZSBubyBkaWZmZXJlbmNlcycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnY2RrIGRpZmYgLS1mYWlsIHdpdGggbXVsdGlwbGUgc3RhY2sgZXhpdHMgd2l0aCBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMScpO1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ1RoZXJlIHdlcmUgbm8gZGlmZmVyZW5jZXMnKTtcblxuICBjb25zdCBkaWZmMiA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyldKTtcbiAgZXhwZWN0KGRpZmYyKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHN0YWNrIHdpdGggZG9ja2VyIGFzc2V0Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdkb2NrZXInKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgYW5kIHRlc3Qgc3RhY2sgd2l0aCBsYW1iZGEgYXNzZXQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnbGFtYmRhJywgeyBjYXB0dXJlU3RkZXJyOiBmYWxzZSB9KTtcblxuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcbiAgY29uc3QgbGFtYmRhQXJuID0gcmVzcG9uc2UuU3RhY2tzPy5bMF0uT3V0cHV0cz8uWzBdLk91dHB1dFZhbHVlO1xuICBpZiAobGFtYmRhQXJuID09PSB1bmRlZmluZWQpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ1N0YWNrIGRpZCBub3QgaGF2ZSBleHBlY3RlZCBMYW1iZGEgQVJOIG91dHB1dCcpO1xuICB9XG5cbiAgY29uc3Qgb3V0cHV0ID0gYXdhaXQgZml4dHVyZS5hd3MubGFtYmRhKCdpbnZva2UnLCB7XG4gICAgRnVuY3Rpb25OYW1lOiBsYW1iZGFBcm4sXG4gIH0pO1xuXG4gIGV4cGVjdChKU09OLnN0cmluZ2lmeShvdXRwdXQuUGF5bG9hZCkpLnRvQ29udGFpbignZGVhciBhc3NldCcpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NkayBscycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBsaXN0aW5nID0gYXdhaXQgZml4dHVyZS5jZGsoWydscyddLCB7IGNhcHR1cmVTdGRlcnI6IGZhbHNlIH0pO1xuXG4gIGNvbnN0IGV4cGVjdGVkU3RhY2tzID0gW1xuICAgICdjb25kaXRpb25hbC1yZXNvdXJjZScsXG4gICAgJ2RvY2tlcicsXG4gICAgJ2RvY2tlci13aXRoLWN1c3RvbS1maWxlJyxcbiAgICAnZmFpbGVkJyxcbiAgICAnaWFtLXRlc3QnLFxuICAgICdsYW1iZGEnLFxuICAgICdtaXNzaW5nLXNzbS1wYXJhbWV0ZXInLFxuICAgICdvcmRlci1wcm92aWRpbmcnLFxuICAgICdvdXRwdXRzLXRlc3QtMScsXG4gICAgJ291dHB1dHMtdGVzdC0yJyxcbiAgICAncGFyYW0tdGVzdC0xJyxcbiAgICAncGFyYW0tdGVzdC0yJyxcbiAgICAncGFyYW0tdGVzdC0zJyxcbiAgICAndGVybWluYXRpb24tcHJvdGVjdGlvbicsXG4gICAgJ3Rlc3QtMScsXG4gICAgJ3Rlc3QtMicsXG4gICAgJ3dpdGgtbmVzdGVkLXN0YWNrJyxcbiAgICAnd2l0aC1uZXN0ZWQtc3RhY2stdXNpbmctcGFyYW1ldGVycycsXG4gICAgJ29yZGVyLWNvbnN1bWluZycsXG4gIF07XG5cbiAgZm9yIChjb25zdCBzdGFjayBvZiBleHBlY3RlZFN0YWNrcykge1xuICAgIGV4cGVjdChsaXN0aW5nKS50b0NvbnRhaW4oZml4dHVyZS5mdWxsU3RhY2tOYW1lKHN0YWNrKSk7XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgc3RhY2sgd2l0aG91dCByZXNvdXJjZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGhvdXQgcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScsIHsgbW9kRW52OiB7IE5PX1JFU09VUkNFOiAnVFJVRScgfSB9KTtcblxuICAvLyBUaGlzIHNob3VsZCBoYXZlIHN1Y2NlZWRlZCBidXQgbm90IGRlcGxveWVkIHRoZSBzdGFjay5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGggcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScpO1xuXG4gIC8vIFRoZW4gYWdhaW4gV0lUSE9VVCByZXNvdXJjZXMgKHRoaXMgc2hvdWxkIGRlc3Ryb3kgdGhlIHN0YWNrKVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnY29uZGl0aW9uYWwtcmVzb3VyY2UnLCB7IG1vZEVudjogeyBOT19SRVNPVVJDRTogJ1RSVUUnIH0gfSk7XG5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdJQU0gZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBvdXRwdXQgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2lhbS10ZXN0JyldKTtcblxuICAvLyBSb3VnaGx5IGNoZWNrIGZvciBhIHRhYmxlIGxpa2UgdGhpczpcbiAgLy9cbiAgLy8g4pSM4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSALeKUgOKUgOKUrOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUkFxuICAvLyDilIIgICDilIIgUmVzb3VyY2UgICAgICAgIOKUgiBFZmZlY3Qg4pSCIEFjdGlvbiAgICAgICAgIOKUgiBQcmluY2lwYWwgICAgICAgICAgICAgICAgICAgICDilIIgQ29uZGl0aW9uIOKUglxuICAvLyDilJzilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilKRcbiAgLy8g4pSCICsg4pSCICR7U29tZVJvbGUuQXJufSDilIIgQWxsb3cgIOKUgiBzdHM6QXNzdW1lUm9sZSDilIIgU2VydmljZTplYzIuYW1hem9uYXdzLmNvbSAgICAg4pSCICAgICAgICAgICDilIJcbiAgLy8g4pSU4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSYXG5cbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCcke1NvbWVSb2xlLkFybn0nKTtcbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCdzdHM6QXNzdW1lUm9sZScpO1xuICBleHBlY3Qob3V0cHV0KS50b0NvbnRhaW4oJ2VjMi5hbWF6b25hd3MuY29tJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZmFzdCBkZXBsb3knLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gd2UgYXJlIHVzaW5nIGEgc3RhY2sgd2l0aCBhIG5lc3RlZCBzdGFjayBiZWNhdXNlIENGTiB3aWxsIGFsd2F5cyBhdHRlbXB0IHRvXG4gIC8vIHVwZGF0ZSBhIG5lc3RlZCBzdGFjaywgd2hpY2ggd2lsbCBhbGxvdyB1cyB0byB2ZXJpZnkgdGhhdCB1cGRhdGVzIGFyZSBhY3R1YWxseVxuICAvLyBza2lwcGVkIHVubGVzcyAtLWZvcmNlIGlzIHNwZWNpZmllZC5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IGNhcHR1cmVTdGRlcnI6IGZhbHNlIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQxID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzYW1lIHN0YWNrIGFnYWluLCB0aGVyZSBzaG91bGQgYmUgbm8gbmV3IGNoYW5nZSBzZXQgY3JlYXRlZFxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snKTtcbiAgY29uc3QgY2hhbmdlU2V0MiA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCkudG9FcXVhbChjaGFuZ2VTZXQxLkNoYW5nZVNldElkKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIGFnYWluIHdpdGggLS1mb3JjZSwgbm93IHdlIHNob3VsZCBjcmVhdGUgYSBjaGFuZ2VzZXRcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3dpdGgtbmVzdGVkLXN0YWNrJywgeyBvcHRpb25zOiBbJy0tZm9yY2UnXSB9KTtcbiAgY29uc3QgY2hhbmdlU2V0MyA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0My5DaGFuZ2VTZXRJZCkubm90LnRvRXF1YWwoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzdGFjayBhZ2FpbiB3aXRoIHRhZ3MsIGV4cGVjdGVkIHRvIGNyZWF0ZSBhIG5ldyBjaGFuZ2VzZXRcbiAgLy8gZXZlbiB0aG91Z2ggdGhlIHJlc291cmNlcyBkaWRuJ3QgY2hhbmdlLlxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IG9wdGlvbnM6IFsnLS10YWdzJywgJ2tleT12YWx1ZSddIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQ0ID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG4gIGV4cGVjdChjaGFuZ2VTZXQ0LkNoYW5nZVNldElkKS5ub3QudG9FcXVhbChjaGFuZ2VTZXQzLkNoYW5nZVNldElkKTtcblxuICBhc3luYyBmdW5jdGlvbiBnZXRMYXRlc3RDaGFuZ2VTZXQoKSB7XG4gICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7IFN0YWNrTmFtZTogc3RhY2tBcm4gfSk7XG4gICAgaWYgKCFyZXNwb25zZS5TdGFja3M/LlswXSkgeyB0aHJvdyBuZXcgRXJyb3IoJ0RpZCBub3QgZ2V0IGEgQ2hhbmdlU2V0IGF0IGFsbCcpOyB9XG4gICAgZml4dHVyZS5sb2coYEZvdW5kIENoYW5nZSBTZXQgJHtyZXNwb25zZS5TdGFja3M/LlswXS5DaGFuZ2VTZXRJZH1gKTtcbiAgICByZXR1cm4gcmVzcG9uc2UuU3RhY2tzPy5bMF07XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdmYWlsZWQgZGVwbG95IGRvZXMgbm90IGhhbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gdGhpcyB3aWxsIGhhbmcgaWYgd2UgaW50cm9kdWNlIGh0dHBzOi8vZ2l0aHViLmNvbS9hd3MvYXdzLWNkay9pc3N1ZXMvNjQwMyBhZ2Fpbi5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrRGVwbG95KCdmYWlsZWQnKSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NhbiBzdGlsbCBsb2FkIG9sZCBhc3NlbWJsaWVzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGN4QXNtRGlyID0gcGF0aC5qb2luKG9zLnRtcGRpcigpLCAnY2RrLWludGVnLWN4Jyk7XG5cbiAgY29uc3QgdGVzdEFzc2VtYmxpZXNEaXJlY3RvcnkgPSBwYXRoLmpvaW4oX19kaXJuYW1lLCAnY2xvdWQtYXNzZW1ibGllcycpO1xuICBmb3IgKGNvbnN0IGFzbWRpciBvZiBhd2FpdCBsaXN0Q2hpbGREaXJzKHRlc3RBc3NlbWJsaWVzRGlyZWN0b3J5KSkge1xuICAgIGZpeHR1cmUubG9nKGBBU1NFTUJMWSAke2FzbWRpcn1gKTtcbiAgICBhd2FpdCBjbG9uZURpcmVjdG9yeShhc21kaXIsIGN4QXNtRGlyKTtcblxuICAgIC8vIFNvbWUgZmlsZXMgaW4gdGhlIGFzbSBkaXJlY3RvcnkgdGhhdCBoYXZlIGEgLmpzIGV4dGVuc2lvbiBhcmVcbiAgICAvLyBhY3R1YWxseSB0cmVhdGVkIGFzIHRlbXBsYXRlcy4gRXZhbHVhdGUgdGhlbSB1c2luZyBOb2RlSlMuXG4gICAgY29uc3QgdGVtcGxhdGVzID0gYXdhaXQgbGlzdENoaWxkcmVuKGN4QXNtRGlyLCBmdWxsUGF0aCA9PiBQcm9taXNlLnJlc29sdmUoZnVsbFBhdGguZW5kc1dpdGgoJy5qcycpKSk7XG4gICAgZm9yIChjb25zdCB0ZW1wbGF0ZSBvZiB0ZW1wbGF0ZXMpIHtcbiAgICAgIGNvbnN0IHRhcmdldE5hbWUgPSB0ZW1wbGF0ZS5yZXBsYWNlKC8uanMkLywgJycpO1xuICAgICAgYXdhaXQgc2hlbGwoW3Byb2Nlc3MuZXhlY1BhdGgsIHRlbXBsYXRlLCAnPicsIHRhcmdldE5hbWVdLCB7XG4gICAgICAgIGN3ZDogY3hBc21EaXIsXG4gICAgICAgIG91dHB1dDogZml4dHVyZS5vdXRwdXQsXG4gICAgICAgIG1vZEVudjoge1xuICAgICAgICAgIFRFU1RfQUNDT1VOVDogYXdhaXQgZml4dHVyZS5hd3MuYWNjb3VudCgpLFxuICAgICAgICAgIFRFU1RfUkVHSU9OOiBmaXh0dXJlLmF3cy5yZWdpb24sXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9XG5cbiAgICAvLyBVc2UgdGhpcyBkaXJlY3RvcnkgYXMgYSBDbG91ZCBBc3NlbWJseVxuICAgIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuY2RrKFtcbiAgICAgICctLWFwcCcsIGN4QXNtRGlyLFxuICAgICAgJy12JyxcbiAgICAgICdzeW50aCcsXG4gICAgXSk7XG5cbiAgICAvLyBBc3NlcnQgdGhhdCB0aGVyZSB3YXMgbm8gcHJvdmlkZXJFcnJvciBpbiBDREsncyBzdGRlcnJcbiAgICAvLyBCZWNhdXNlIHdlIHJlbHkgb24gdGhlIGFwcC9mcmFtZXdvcmsgdG8gYWN0dWFsbHkgZXJyb3IgaW4gY2FzZSB0aGVcbiAgICAvLyBwcm92aWRlciBmYWlscywgd2UgaW5zcGVjdCB0aGUgbG9ncyBoZXJlLlxuICAgIGV4cGVjdChvdXRwdXQpLm5vdC50b0NvbnRhaW4oJyRwcm92aWRlckVycm9yJyk7XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdnZW5lcmF0aW5nIGFuZCBsb2FkaW5nIGFzc2VtYmx5Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGFzbU91dHB1dERpciA9IGAke2ZpeHR1cmUuaW50ZWdUZXN0RGlyfS1jZGstaW50ZWctYXNtYDtcbiAgYXdhaXQgZml4dHVyZS5zaGVsbChbJ3JtJywgJy1yZicsIGFzbU91dHB1dERpcl0pO1xuXG4gIC8vIFN5bnRoZXNpemUgYSBDbG91ZCBBc3NlbWJseSB0b3RoZSBkZWZhdWx0IGRpcmVjdG9yeSAoY2RrLm91dCkgYW5kIGEgc3BlY2lmaWMgZGlyZWN0b3J5LlxuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJ10pO1xuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJywgJy0tb3V0cHV0JywgYXNtT3V0cHV0RGlyXSk7XG5cbiAgLy8gY2RrLm91dCBpbiB0aGUgY3VycmVudCBkaXJlY3RvcnkgYW5kIHRoZSBpbmRpY2F0ZWQgLS1vdXRwdXQgc2hvdWxkIGJlIHRoZSBzYW1lXG4gIGF3YWl0IGZpeHR1cmUuc2hlbGwoWydkaWZmJywgJ2Nkay5vdXQnLCBhc21PdXRwdXREaXJdKTtcblxuICAvLyBDaGVjayB0aGF0IHdlIGNhbiAnbHMnIHRoZSBzeW50aGVzaXplZCBhc20uXG4gIC8vIENoYW5nZSB0byBzb21lIHJhbmRvbSBkaXJlY3RvcnkgdG8gbWFrZSBzdXJlIHdlJ3JlIG5vdCBhY2NpZGVudGFsbHkgbG9hZGluZyBjZGsuanNvblxuICBjb25zdCBsaXN0ID0gYXdhaXQgZml4dHVyZS5jZGsoWyctLWFwcCcsIGFzbU91dHB1dERpciwgJ2xzJ10sIHsgY3dkOiBvcy50bXBkaXIoKSB9KTtcbiAgLy8gU2FtZSBzdGFja3Mgd2Uga25vdyBhcmUgaW4gdGhlIGFwcFxuICBleHBlY3QobGlzdCkudG9Db250YWluKGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1sYW1iZGFgKTtcbiAgZXhwZWN0KGxpc3QpLnRvQ29udGFpbihgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0xYCk7XG4gIGV4cGVjdChsaXN0KS50b0NvbnRhaW4oYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMmApO1xuXG4gIC8vIENoZWNrIHRoYXQgd2UgY2FuIHVzZSAnLicgYW5kIGp1c3Qgc3ludGggLHRoZSBnZW5lcmF0ZWQgYXNtXG4gIGNvbnN0IHN0YWNrVGVtcGxhdGUgPSBhd2FpdCBmaXh0dXJlLmNkayhbJy0tYXBwJywgJy4nLCAnc3ludGgnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMicpXSwge1xuICAgIGN3ZDogYXNtT3V0cHV0RGlyLFxuICB9KTtcbiAgZXhwZWN0KHN0YWNrVGVtcGxhdGUpLnRvQ29udGFpbigndG9waWMxNTJEODRBMzcnKTtcblxuICAvLyBEZXBsb3kgYSBMYW1iZGEgZnJvbSB0aGUgY29waWVkIGFzbVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnbGFtYmRhJywgeyBvcHRpb25zOiBbJy1hJywgJy4nXSwgY3dkOiBhc21PdXRwdXREaXIgfSk7XG5cbiAgLy8gUmVtb3ZlIChyZW5hbWUpIHRoZSBvcmlnaW5hbCBjdXN0b20gZG9ja2VyIGZpbGUgdGhhdCB3YXMgdXNlZCBkdXJpbmcgc3ludGguXG4gIC8vIHRoaXMgdmVyaWZpZXMgdGhhdCB0aGUgYXNzZW1seSBoYXMgYSBjb3B5IG9mIGl0IGFuZCB0aGF0IHRoZSBtYW5pZmVzdCB1c2VzXG4gIC8vIHJlbGF0aXZlIHBhdGhzIHRvIHJlZmVyZW5jZSB0byBpdC5cbiAgY29uc3QgY3VzdG9tRG9ja2VyRmlsZSA9IHBhdGguam9pbihmaXh0dXJlLmludGVnVGVzdERpciwgJ2RvY2tlcicsICdEb2NrZXJmaWxlLkN1c3RvbScpO1xuICBhd2FpdCBmcy5yZW5hbWUoY3VzdG9tRG9ja2VyRmlsZSwgYCR7Y3VzdG9tRG9ja2VyRmlsZX1+YCk7XG4gIHRyeSB7XG5cbiAgICAvLyBkZXBsb3kgYSBkb2NrZXIgaW1hZ2Ugd2l0aCBjdXN0b20gZmlsZSB3aXRob3V0IHN5bnRoICh1c2VzIGFzc2V0cylcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZG9ja2VyLXdpdGgtY3VzdG9tLWZpbGUnLCB7IG9wdGlvbnM6IFsnLWEnLCAnLiddLCBjd2Q6IGFzbU91dHB1dERpciB9KTtcblxuICB9IGZpbmFsbHkge1xuICAgIC8vIFJlbmFtZSBiYWNrIHRvIHJlc3RvcmUgZml4dHVyZSB0byBvcmlnaW5hbCBzdGF0ZVxuICAgIGF3YWl0IGZzLnJlbmFtZShgJHtjdXN0b21Eb2NrZXJGaWxlfX5gLCBjdXN0b21Eb2NrZXJGaWxlKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ3RlbXBsYXRlcyBvbiBkaXNrIGNvbnRhaW4gbWV0YWRhdGEgcmVzb3VyY2UsIGFsc28gaW4gbmVzdGVkIGFzc2VtYmxpZXMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gU3ludGggZmlyc3QsIGFuZCBzd2l0Y2ggb24gdmVyc2lvbiByZXBvcnRpbmcgYmVjYXVzZSBjZGsuanNvbiBpcyBkaXNhYmxpbmcgaXRcbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsICctLXZlcnNpb24tcmVwb3J0aW5nPXRydWUnXSk7XG5cbiAgLy8gTG9hZCB0ZW1wbGF0ZSBmcm9tIGRpc2sgZnJvbSByb290IGFzc2VtYmx5XG4gIGNvbnN0IHRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvKi1sYW1iZGEudGVtcGxhdGUuanNvbiddKTtcblxuICBleHBlY3QoSlNPTi5wYXJzZSh0ZW1wbGF0ZUNvbnRlbnRzKS5SZXNvdXJjZXMuQ0RLTWV0YWRhdGEpLnRvQmVUcnV0aHkoKTtcblxuICAvLyBMb2FkIHRlbXBsYXRlIGZyb20gbmVzdGVkIGFzc2VtYmx5XG4gIGNvbnN0IG5lc3RlZFRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvYXNzZW1ibHktKi1zdGFnZS8qLXN0YWdlLVN0YWNrSW5TdGFnZS50ZW1wbGF0ZS5qc29uJ10pO1xuXG4gIGV4cGVjdChKU09OLnBhcnNlKG5lc3RlZFRlbXBsYXRlQ29udGVudHMpLlJlc291cmNlcy5DREtNZXRhZGF0YSkudG9CZVRydXRoeSgpO1xufSkpO1xuXG5hc3luYyBmdW5jdGlvbiBsaXN0Q2hpbGRyZW4ocGFyZW50OiBzdHJpbmcsIHByZWQ6ICh4OiBzdHJpbmcpID0+IFByb21pc2U8Ym9vbGVhbj4pIHtcbiAgY29uc3QgcmV0ID0gbmV3IEFycmF5PHN0cmluZz4oKTtcbiAgZm9yIChjb25zdCBjaGlsZCBvZiBhd2FpdCBmcy5yZWFkZGlyKHBhcmVudCwgeyBlbmNvZGluZzogJ3V0Zi04JyB9KSkge1xuICAgIGNvbnN0IGZ1bGxQYXRoID0gcGF0aC5qb2luKHBhcmVudCwgY2hpbGQudG9TdHJpbmcoKSk7XG4gICAgaWYgKGF3YWl0IHByZWQoZnVsbFBhdGgpKSB7XG4gICAgICByZXQucHVzaChmdWxsUGF0aCk7XG4gICAgfVxuICB9XG4gIHJldHVybiByZXQ7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGxpc3RDaGlsZERpcnMocGFyZW50OiBzdHJpbmcpIHtcbiAgcmV0dXJuIGxpc3RDaGlsZHJlbihwYXJlbnQsIGFzeW5jIChmdWxsUGF0aDogc3RyaW5nKSA9PiAoYXdhaXQgZnMuc3RhdChmdWxsUGF0aCkpLmlzRGlyZWN0b3J5KCkpO1xufVxuIl19 \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.67.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.67.0/NOTES.md new file mode 100644 index 000000000..19bd85e22 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.67.0/NOTES.md @@ -0,0 +1,2 @@ +Integration tests are now injected with environment variable that specifies which framework +version they should install and use. This patch includes the cdk-helpers.js file that is responsible for that. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.67.0/cdk-helpers.js b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.67.0/cdk-helpers.js new file mode 100644 index 000000000..308e45722 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v1.67.0/cdk-helpers.js @@ -0,0 +1,331 @@ +"use strict"; +var _a, _b; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.randomString = exports.rimraf = exports.shell = exports.TestFixture = exports.cloneDirectory = exports.withDefaultFixture = exports.withCdkApp = exports.withAws = void 0; +const child_process = require("child_process"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const aws_helpers_1 = require("./aws-helpers"); +const resource_pool_1 = require("./resource-pool"); +const REGIONS = process.env.AWS_REGIONS + ? process.env.AWS_REGIONS.split(',') + : [(_b = (_a = process.env.AWS_REGION) !== null && _a !== void 0 ? _a : process.env.AWS_DEFAULT_REGION) !== null && _b !== void 0 ? _b : 'us-east-1']; +const FRAMEWORK_VERSION = process.env.FRAMEWORK_VERSION; +process.stdout.write(`Using regions: ${REGIONS}\n`); +process.stdout.write(`Using framework version: ${FRAMEWORK_VERSION}\n`); +const REGION_POOL = new resource_pool_1.ResourcePool(REGIONS); +/** + * Higher order function to execute a block with an AWS client setup + * + * Allocate the next region from the REGION pool and dispose it afterwards. + */ +function withAws(block) { + return (context) => REGION_POOL.using(async (region) => { + const aws = await aws_helpers_1.AwsClients.forRegion(region, context.output); + await sanityCheck(aws); + return block({ ...context, aws }); + }); +} +exports.withAws = withAws; +/** + * Higher order function to execute a block with a CDK app fixture + * + * Requires an AWS client to be passed in. + * + * For backwards compatibility with existing tests (so we don't have to change + * too much) the inner block is expected to take a `TestFixture` object. + */ +function withCdkApp(block) { + return async (context) => { + const randy = randomString(); + const stackNamePrefix = `cdktest-${randy}`; + const integTestDir = path.join(os.tmpdir(), `cdk-integ-${randy}`); + context.output.write(` Stack prefix: ${stackNamePrefix}\n`); + context.output.write(` Test directory: ${integTestDir}\n`); + context.output.write(` Region: ${context.aws.region}\n`); + await cloneDirectory(path.join(__dirname, 'app'), integTestDir, context.output); + const fixture = new TestFixture(integTestDir, stackNamePrefix, context.output, context.aws); + let success = true; + try { + let modules = [ + '@aws-cdk/core', + '@aws-cdk/aws-sns', + '@aws-cdk/aws-iam', + '@aws-cdk/aws-lambda', + '@aws-cdk/aws-ssm', + '@aws-cdk/aws-ecr-assets', + '@aws-cdk/aws-cloudformation', + '@aws-cdk/aws-ec2', + ]; + if (FRAMEWORK_VERSION) { + modules = modules.map(module => `${module}@${FRAMEWORK_VERSION}`); + } + await fixture.shell(['npm', 'install', ...modules]); + await ensureBootstrapped(fixture); + await block(fixture); + } + catch (e) { + success = false; + throw e; + } + finally { + await fixture.dispose(success); + } + }; +} +exports.withCdkApp = withCdkApp; +/** + * Default test fixture for most (all?) integ tests + * + * It's a composition of withAws/withCdkApp, expecting the test block to take a `TestFixture` + * object. + * + * We could have put `withAws(withCdkApp(fixture => { /... actual test here.../ }))` in every + * test declaration but centralizing it is going to make it convenient to modify in the future. + */ +function withDefaultFixture(block) { + return withAws(withCdkApp(block)); + // ^~~~~~ this is disappointing TypeScript! Feels like you should have been able to derive this. +} +exports.withDefaultFixture = withDefaultFixture; +/** + * Prepare a target dir byreplicating a source directory + */ +async function cloneDirectory(source, target, output) { + await shell(['rm', '-rf', target], { output }); + await shell(['mkdir', '-p', target], { output }); + await shell(['cp', '-R', source + '/*', target], { output }); +} +exports.cloneDirectory = cloneDirectory; +class TestFixture { + constructor(integTestDir, stackNamePrefix, output, aws) { + this.integTestDir = integTestDir; + this.stackNamePrefix = stackNamePrefix; + this.output = output; + this.aws = aws; + this.qualifier = randomString().slice(0, 10); + this.bucketsToDelete = new Array(); + } + log(s) { + this.output.write(`${s}\n`); + } + async shell(command, options = {}) { + return shell(command, { + output: this.output, + cwd: this.integTestDir, + ...options, + }); + } + async cdkDeploy(stackNames, options = {}) { + var _a, _b; + stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + const neverRequireApproval = (_a = options.neverRequireApproval) !== null && _a !== void 0 ? _a : true; + return this.cdk(['deploy', + ...(neverRequireApproval ? ['--require-approval=never'] : []), // Default to no approval in an unattended test + ...((_b = options.options) !== null && _b !== void 0 ? _b : []), ...this.fullStackName(stackNames)], options); + } + async cdkDestroy(stackNames, options = {}) { + var _a; + stackNames = typeof stackNames === 'string' ? [stackNames] : stackNames; + return this.cdk(['destroy', + '-f', // We never want a prompt in an unattended test + ...((_a = options.options) !== null && _a !== void 0 ? _a : []), ...this.fullStackName(stackNames)], options); + } + async cdk(args, options = {}) { + var _a; + const verbose = (_a = options.verbose) !== null && _a !== void 0 ? _a : true; + return this.shell(['cdk', ...(verbose ? ['-v'] : []), ...args], { + ...options, + modEnv: { + AWS_REGION: this.aws.region, + AWS_DEFAULT_REGION: this.aws.region, + STACK_NAME_PREFIX: this.stackNamePrefix, + ...options.modEnv, + }, + }); + } + fullStackName(stackNames) { + if (typeof stackNames === 'string') { + return `${this.stackNamePrefix}-${stackNames}`; + } + else { + return stackNames.map(s => `${this.stackNamePrefix}-${s}`); + } + } + /** + * Append this to the list of buckets to potentially delete + * + * At the end of a test, we clean up buckets that may not have gotten destroyed + * (for whatever reason). + */ + rememberToDeleteBucket(bucketName) { + this.bucketsToDelete.push(bucketName); + } + /** + * Cleanup leftover stacks and buckets + */ + async dispose(success) { + const stacksToDelete = await this.deleteableStacks(this.stackNamePrefix); + // Bootstrap stacks have buckets that need to be cleaned + const bucketNames = stacksToDelete.map(stack => aws_helpers_1.outputFromStack('BucketName', stack)).filter(defined); + await Promise.all(bucketNames.map(b => this.aws.emptyBucket(b))); + // Bootstrap stacks have ECR repositories with images which should be deleted + const imageRepositoryNames = stacksToDelete.map(stack => aws_helpers_1.outputFromStack('ImageRepositoryName', stack)).filter(defined); + await Promise.all(imageRepositoryNames.map(r => this.aws.deleteImageRepository(r))); + await this.aws.deleteStacks(...stacksToDelete.map(s => s.StackName)); + // We might have leaked some buckets by upgrading the bootstrap stack. Be + // sure to clean everything. + for (const bucket of this.bucketsToDelete) { + await this.aws.deleteBucket(bucket); + } + // If the tests completed successfully, happily delete the fixture + // (otherwise leave it for humans to inspect) + if (success) { + rimraf(this.integTestDir); + } + } + /** + * Return the stacks starting with our testing prefix that should be deleted + */ + async deleteableStacks(prefix) { + var _a; + const statusFilter = [ + 'CREATE_IN_PROGRESS', 'CREATE_FAILED', 'CREATE_COMPLETE', + 'ROLLBACK_IN_PROGRESS', 'ROLLBACK_FAILED', 'ROLLBACK_COMPLETE', + 'DELETE_FAILED', + 'UPDATE_IN_PROGRESS', 'UPDATE_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_COMPLETE', 'UPDATE_ROLLBACK_IN_PROGRESS', + 'UPDATE_ROLLBACK_FAILED', + 'UPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESS', + 'UPDATE_ROLLBACK_COMPLETE', 'REVIEW_IN_PROGRESS', + 'IMPORT_IN_PROGRESS', 'IMPORT_COMPLETE', + 'IMPORT_ROLLBACK_IN_PROGRESS', 'IMPORT_ROLLBACK_FAILED', + 'IMPORT_ROLLBACK_COMPLETE', + ]; + const response = await this.aws.cloudFormation('describeStacks', {}); + return ((_a = response.Stacks) !== null && _a !== void 0 ? _a : []) + .filter(s => s.StackName.startsWith(prefix)) + .filter(s => statusFilter.includes(s.StackStatus)) + .filter(s => s.RootId === undefined); // Only delete parent stacks. Nested stacks are deleted in the process + } +} +exports.TestFixture = TestFixture; +/** + * Perform a one-time quick sanity check that the AWS clients has properly configured credentials + * + * If we don't do this, calls are going to fail and they'll be retried and everything will take + * forever before the user notices a simple misconfiguration. + * + * We can't check for the presence of environment variables since credentials could come from + * anywhere, so do simple account retrieval. + * + * Only do it once per process. + */ +async function sanityCheck(aws) { + if (sanityChecked === undefined) { + try { + await aws.account(); + sanityChecked = true; + } + catch (e) { + sanityChecked = false; + throw new Error(`AWS credentials probably not configured, got error: ${e.message}`); + } + } + if (!sanityChecked) { + throw new Error('AWS credentials probably not configured, see previous error'); + } +} +let sanityChecked; +/** + * Make sure that the given environment is bootstrapped + * + * Since we go striping across regions, it's going to suck doing this + * by hand so let's just mass-automate it. + */ +async function ensureBootstrapped(fixture) { + // Old-style bootstrap stack with default name + if (await fixture.aws.stackStatus('CDKToolkit') === undefined) { + await fixture.cdk(['bootstrap', `aws://${await fixture.aws.account()}/${fixture.aws.region}`]); + } +} +/** + * A shell command that does what you want + * + * Is platform-aware, handles errors nicely. + */ +async function shell(command, options = {}) { + var _a, _b; + if (options.modEnv && options.env) { + throw new Error('Use either env or modEnv but not both'); + } + (_a = options.output) === null || _a === void 0 ? void 0 : _a.write(`💻 ${command.join(' ')}\n`); + const env = (_b = options.env) !== null && _b !== void 0 ? _b : (options.modEnv ? { ...process.env, ...options.modEnv } : undefined); + const child = child_process.spawn(command[0], command.slice(1), { + ...options, + env, + // Need this for Windows where we want .cmd and .bat to be found as well. + shell: true, + stdio: ['ignore', 'pipe', 'pipe'], + }); + return new Promise((resolve, reject) => { + const stdout = new Array(); + const stderr = new Array(); + child.stdout.on('data', chunk => { + var _a; + (_a = options.output) === null || _a === void 0 ? void 0 : _a.write(chunk); + stdout.push(chunk); + }); + child.stderr.on('data', chunk => { + var _a, _b; + (_a = options.output) === null || _a === void 0 ? void 0 : _a.write(chunk); + if ((_b = options.captureStderr) !== null && _b !== void 0 ? _b : true) { + stderr.push(chunk); + } + }); + child.once('error', reject); + child.once('close', code => { + if (code === 0 || options.allowErrExit) { + resolve((Buffer.concat(stdout).toString('utf-8') + Buffer.concat(stderr).toString('utf-8')).trim()); + } + else { + reject(new Error(`'${command.join(' ')}' exited with error code ${code}`)); + } + }); + }); +} +exports.shell = shell; +function defined(x) { + return x !== undefined; +} +/** + * rm -rf reimplementation, don't want to depend on an NPM package for this + */ +function rimraf(fsPath) { + try { + const isDir = fs.lstatSync(fsPath).isDirectory(); + if (isDir) { + for (const file of fs.readdirSync(fsPath)) { + rimraf(path.join(fsPath, file)); + } + fs.rmdirSync(fsPath); + } + else { + fs.unlinkSync(fsPath); + } + } + catch (e) { + // We will survive ENOENT + if (e.code !== 'ENOENT') { + throw e; + } + } +} +exports.rimraf = rimraf; +function randomString() { + // Crazy + return Math.random().toString(36).replace(/[^a-z0-9]+/g, ''); +} +exports.randomString = randomString; +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2RrLWhlbHBlcnMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJjZGstaGVscGVycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7O0FBQUEsK0NBQStDO0FBQy9DLHlCQUF5QjtBQUN6Qix5QkFBeUI7QUFDekIsNkJBQTZCO0FBQzdCLCtDQUE0RDtBQUM1RCxtREFBK0M7QUFHL0MsTUFBTSxPQUFPLEdBQUcsT0FBTyxDQUFDLEdBQUcsQ0FBQyxXQUFXO0lBQ3JDLENBQUMsQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLFdBQVcsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDO0lBQ3BDLENBQUMsQ0FBQyxhQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsVUFBVSxtQ0FBSSxPQUFPLENBQUMsR0FBRyxDQUFDLGtCQUFrQixtQ0FBSSxXQUFXLENBQUMsQ0FBQztBQUU5RSxNQUFNLGlCQUFpQixHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsaUJBQWlCLENBQUM7QUFFeEQsT0FBTyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsa0JBQWtCLE9BQU8sSUFBSSxDQUFDLENBQUM7QUFDcEQsT0FBTyxDQUFDLE1BQU0sQ0FBQyxLQUFLLENBQUMsNEJBQTRCLGlCQUFpQixJQUFJLENBQUMsQ0FBQztBQUV4RSxNQUFNLFdBQVcsR0FBRyxJQUFJLDRCQUFZLENBQUMsT0FBTyxDQUFDLENBQUM7QUFLOUM7Ozs7R0FJRztBQUNILFNBQWdCLE9BQU8sQ0FBd0IsS0FBaUQ7SUFDOUYsT0FBTyxDQUFDLE9BQVUsRUFBRSxFQUFFLENBQUMsV0FBVyxDQUFDLEtBQUssQ0FBQyxLQUFLLEVBQUUsTUFBTSxFQUFFLEVBQUU7UUFDeEQsTUFBTSxHQUFHLEdBQUcsTUFBTSx3QkFBVSxDQUFDLFNBQVMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQy9ELE1BQU0sV0FBVyxDQUFDLEdBQUcsQ0FBQyxDQUFDO1FBRXZCLE9BQU8sS0FBSyxDQUFDLEVBQUUs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QUFDekIsQ0FBQztBQUVEOztHQUVHO0FBQ0gsU0FBZ0IsTUFBTSxDQUFDLE1BQWM7SUFDbkMsSUFBSTtRQUNGLE1BQU0sS0FBSyxHQUFHLEVBQUUsQ0FBQyxTQUFTLENBQUMsTUFBTSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUM7UUFFakQsSUFBSSxLQUFLLEVBQUU7WUFDVCxLQUFLLE1BQU0sSUFBSSxJQUFJLEVBQUUsQ0FBQyxXQUFXLENBQUMsTUFBTSxDQUFDLEVBQUU7Z0JBQ3pDLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLE1BQU0sRUFBRSxJQUFJLENBQUMsQ0FBQyxDQUFDO2FBQ2pDO1lBQ0QsRUFBRSxDQUFDLFNBQVMsQ0FBQyxNQUFNLENBQUMsQ0FBQztTQUN0QjthQUFNO1lBQ0wsRUFBRSxDQUFDLFVBQVUsQ0FBQyxNQUFNLENBQUMsQ0FBQztTQUN2QjtLQUNGO0lBQUMsT0FBTyxDQUFDLEVBQUU7UUFDVix5QkFBeUI7UUFDekIsSUFBSSxDQUFDLENBQUMsSUFBSSxLQUFLLFFBQVEsRUFBRTtZQUFFLE1BQU0sQ0FBQyxDQUFDO1NBQUU7S0FDdEM7QUFDSCxDQUFDO0FBaEJELHdCQWdCQztBQUVELFNBQWdCLFlBQVk7SUFDMUIsUUFBUTtJQUNSLE9BQU8sSUFBSSxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUMsQ0FBQyxPQUFPLENBQUMsYUFBYSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBQy9ELENBQUM7QUFIRCxvQ0FHQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCAqIGFzIGNoaWxkX3Byb2Nlc3MgZnJvbSAnY2hpbGRfcHJvY2Vzcyc7XG5pbXBvcnQgKiBhcyBmcyBmcm9tICdmcyc7XG5pbXBvcnQgKiBhcyBvcyBmcm9tICdvcyc7XG5pbXBvcnQgKiBhcyBwYXRoIGZyb20gJ3BhdGgnO1xuaW1wb3J0IHsgb3V0cHV0RnJvbVN0YWNrLCBBd3NDbGllbnRzIH0gZnJvbSAnLi9hd3MtaGVscGVycyc7XG5pbXBvcnQgeyBSZXNvdXJjZVBvb2wgfSBmcm9tICcuL3Jlc291cmNlLXBvb2wnO1xuaW1wb3J0IHsgVGVzdENvbnRleHQgfSBmcm9tICcuL3Rlc3QtaGVscGVycyc7XG5cbmNvbnN0IFJFR0lPTlMgPSBwcm9jZXNzLmVudi5BV1NfUkVHSU9OU1xuICA/IHByb2Nlc3MuZW52LkFXU19SRUdJT05TLnNwbGl0KCcsJylcbiAgOiBbcHJvY2Vzcy5lbnYuQVdTX1JFR0lPTiA/PyBwcm9jZXNzLmVudi5BV1NfREVGQVVMVF9SRUdJT04gPz8gJ3VzLWVhc3QtMSddO1xuXG5jb25zdCBGUkFNRVdPUktfVkVSU0lPTiA9IHByb2Nlc3MuZW52LkZSQU1FV09SS19WRVJTSU9OO1xuXG5wcm9jZXNzLnN0ZG91dC53cml0ZShgVXNpbmcgcmVnaW9uczogJHtSRUdJT05TfVxcbmApO1xucHJvY2Vzcy5zdGRvdXQud3JpdGUoYFVzaW5nIGZyYW1ld29yayB2ZXJzaW9uOiAke0ZSQU1FV09SS19WRVJTSU9OfVxcbmApO1xuXG5jb25zdCBSRUdJT05fUE9PTCA9IG5ldyBSZXNvdXJjZVBvb2woUkVHSU9OUyk7XG5cblxuZXhwb3J0IHR5cGUgQXdzQ29udGV4dCA9IHsgcmVhZG9ubHkgYXdzOiBBd3NDbGllbnRzIH07XG5cbi8qKlxuICogSGlnaGVyIG9yZGVyIGZ1bmN0aW9uIHRvIGV4ZWN1dGUgYSBibG9jayB3aXRoIGFuIEFXUyBjbGllbnQgc2V0dXBcbiAqXG4gKiBBbGxvY2F0ZSB0aGUgbmV4dCByZWdpb24gZnJvbSB0aGUgUkVHSU9OIHBvb2wgYW5kIGRpc3Bvc2UgaXQgYWZ0ZXJ3YXJkcy5cbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHdpdGhBd3M8QSBleHRlbmRzIFRlc3RDb250ZXh0PihibG9jazogKGNvbnRleHQ6IEEgJiBBd3NDb250ZXh0KSA9PiBQcm9taXNlPHZvaWQ+KSB7XG4gIHJldHVybiAoY29udGV4dDogQSkgPT4gUkVHSU9OX1BPT0wudXNpbmcoYXN5bmMgKHJlZ2lvbikgPT4ge1xuICAgIGNvbnN0IGF3cyA9IGF3YWl0IEF3c0NsaWVudHMuZm9yUmVnaW9uKHJlZ2lvbiwgY29udGV4dC5vdXRwdXQpO1xuICAgIGF3YWl0IHNhbml0eUNoZWNrKGF3cyk7XG5cbiAgICByZXR1cm4gYmxvY2soeyAuLi5jb250ZXh0LCBhd3MgfSk7XG4gIH0pO1xufVxuXG4vKipcbiAqIEhpZ2hlciBvcmRlciBmdW5jdGlvbiB0byBleGVjdXRlIGEgYmxvY2sgd2l0aCBhIENESyBhcHAgZml4dHVyZVxuICpcbiAqIFJlcXVpcmVzIGFuIEFXUyBjbGllbnQgdG8gYmUgcGFzc2VkIGluLlxuICpcbiAqIEZvciBiYWNrd2FyZHMgY29tcGF0aWJpbGl0eSB3aXRoIGV4aXN0aW5nIHRlc3RzIChzbyB3ZSBkb24ndCBoYXZlIHRvIGNoYW5nZVxuICogdG9vIG11Y2gpIHRoZSBpbm5lciBibG9jayBpcyBleHBlY3RlZCB0byB0YWtlIGEgYFRlc3RGaXh0dXJlYCBvYmplY3QuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiB3aXRoQ2RrQXBwPEEgZXh0ZW5kcyBUZXN0Q29udGV4dCAmIEF3c0NvbnRleHQ+KGJsb2NrOiAoY29udGV4dDogVGVzdEZpeHR1cmUpID0+IFByb21pc2U8dm9pZD4pIHtcbiAgcmV0dXJuIGFzeW5jIChjb250ZXh0OiBBKSA9PiB7XG4gICAgY29uc3QgcmFuZHkgPSByYW5kb21TdHJpbmcoKTtcbiAgICBjb25zdCBzdGFja05hbWVQcmVmaXggPSBgY2RrdGVzdC0ke3JhbmR5fWA7XG4gICAgY29uc3QgaW50ZWdUZXN0RGlyID0gcGF0aC5qb2luKG9zLnRtcGRpcigpLCBgY2RrLWludGVnLSR7cmFuZHl9YCk7XG5cbiAgICBjb250ZXh0Lm91dHB1dC53cml0ZShgIFN0YWNrIHByZWZpeDogICAke3N0YWNrTmFtZVByZWZpeH1cXG5gKTtcbiAgICBjb250ZXh0Lm91dHB1dC53cml0ZShgIFRlc3QgZGlyZWN0b3J5OiAke2ludGVnVGVzdERpcn1cXG5gKTtcbiAgICBjb250ZXh0Lm91dHB1dC53cml0ZShgIFJlZ2lvbjogICAgICAgICAke2NvbnRleHQuYXdzLnJlZ2lvbn1cXG5gKTtcblxuICAgIGF3YWl0IGNsb25lRGlyZWN0b3J5KHBhdGguam9pbihfX2Rpcm5hbWUsICdhcHAnKSwgaW50ZWdUZXN0RGlyLCBjb250ZXh0Lm91dHB1dCk7XG4gICAgY29uc3QgZml4dHVyZSA9IG5ldyBUZXN0Rml4dHVyZShcbiAgICAgIGludGVnVGVzdERpcixcbiAgICAgIHN0YWNrTmFtZVByZWZpeCxcbiAgICAgIGNvbnRleHQub3V0cHV0LFxuICAgICAgY29udGV4dC5hd3MpO1xuXG4gICAgbGV0IHN1Y2Nlc3MgPSB0cnVlO1xuICAgIHRyeSB7XG4gICAgICBsZXQgbW9kdWxlcyA9IFtcbiAgICAgICAgJ0Bhd3MtY2RrL2NvcmUnLFxuICAgICAgICAnQGF3cy1jZGsvYXdzLXNucycsXG4gICAgICAgICdAYXdzLWNkay9hd3MtaWFtJyxcbiAgICAgICAgJ0Bhd3MtY2RrL2F3cy1sYW1iZGEnLFxuICAgICAgICAnQGF3cy1jZGsvYXdzLXNzbScsXG4gICAgICAgICdAYXdzLWNkay9hd3MtZWNyLWFzc2V0cycsXG4gICAgICAgICdAYXdzLWNkay9hd3MtY2xvdWRmb3JtYXRpb24nLFxuICAgICAgICAnQGF3cy1jZGsvYXdzLWVjMicsXG4gICAgICBdO1xuICAgICAgaWYgKEZSQU1FV09SS19WRVJTSU9OKSB7XG4gICAgICAgIG1vZHVsZXMgPSBtb2R1bGVzLm1hcChtb2R1bGUgPT4gYCR7bW9kdWxlfUAke0ZSQU1FV09SS19WRVJTSU9OfWApO1xuICAgICAgfVxuICAgICAgYXdhaXQgZml4dHVyZS5zaGVsbChbJ25wbScsICdpbnN0YWxsJywgLi4ubW9kdWxlc10pO1xuXG4gICAgICBhd2FpdCBlbnN1cmVCb290c3RyYXBwZWQoZml4dHVyZSk7XG5cbiAgICAgIGF3YWl0IGJsb2NrKGZpeHR1cmUpO1xuICAgIH0gY2F0Y2ggKGUpIHtcbiAgICAgIHN1Y2Nlc3MgPSBmYWxzZTtcbiAgICAgIHRocm93IGU7XG4gICAgfSBmaW5hbGx5IHtcbiAgICAgIGF3YWl0IGZpeHR1cmUuZGlzcG9zZShzdWNjZXNzKTtcbiAgICB9XG4gIH07XG59XG5cbi8qKlxuICogRGVmYXVsdCB0ZXN0IGZpeHR1cmUgZm9yIG1vc3QgKGFsbD8pIGludGVnIHRlc3RzXG4gKlxuICogSXQncyBhIGNvbXBvc2l0aW9uIG9mIHdpdGhBd3Mvd2l0aENka0FwcCwgZXhwZWN0aW5nIHRoZSB0ZXN0IGJsb2NrIHRvIHRha2UgYSBgVGVzdEZpeHR1cmVgXG4gKiBvYmplY3QuXG4gKlxuICogV2UgY291bGQgaGF2ZSBwdXQgYHdpdGhBd3Mod2l0aENka0FwcChmaXh0dXJlID0+IHsgLy4uLiBhY3R1YWwgdGVzdCBoZXJlLi4uLyB9KSlgIGluIGV2ZXJ5XG4gKiB0ZXN0IGRlY2xhcmF0aW9uIGJ1dCBjZW50cmFsaXppbmcgaXQgaXMgZ29pbmcgdG8gbWFrZSBpdCBjb252ZW5pZW50IHRvIG1vZGlmeSBpbiB0aGUgZnV0dXJlLlxuICovXG5leHBvcnQgZnVuY3Rpb24gd2l0aERlZmF1bHRGaXh0dXJlKGJsb2NrOiAoY29udGV4dDogVGVzdEZpeHR1cmUpID0+IFByb21pc2U8dm9pZD4pIHtcbiAgcmV0dXJuIHdpdGhBd3M8VGVzdENvbnRleHQ+KHdpdGhDZGtBcHAoYmxvY2spKTtcbiAgLy8gICAgICAgICAgICAgIF5+fn5+fiB0aGlzIGlzIGRpc2FwcG9pbnRpbmcgVHlwZVNjcmlwdCEgRmVlbHMgbGlrZSB5b3Ugc2hvdWxkIGhhdmUgYmVlbiBhYmxlIHRvIGRlcml2ZSB0aGlzLlxufVxuXG5leHBvcnQgaW50ZXJmYWNlIFNoZWxsT3B0aW9ucyBleHRlbmRzIGNoaWxkX3Byb2Nlc3MuU3Bhd25PcHRpb25zIHtcbiAgLyoqXG4gICAqIFByb3BlcnRpZXMgdG8gYWRkIHRvICdlbnYnXG4gICAqL1xuICBtb2RFbnY/OiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+O1xuXG4gIC8qKlxuICAgKiBEb24ndCBmYWlsIHdoZW4gZXhpdGluZyB3aXRoIGFuIGVycm9yXG4gICAqXG4gICAqIEBkZWZhdWx0IGZhbHNlXG4gICAqL1xuICBhbGxvd0VyckV4aXQ/OiBib29sZWFuO1xuXG4gIC8qKlxuICAgKiBXaGV0aGVyIHRvIGNhcHR1cmUgc3RkZXJyXG4gICAqXG4gICAqIEBkZWZhdWx0IHRydWVcbiAgICovXG4gIGNhcHR1cmVTdGRlcnI/OiBib29sZWFuO1xuXG4gIC8qKlxuICAgKiBQYXNzIG91dHB1dCBoZXJlXG4gICAqL1xuICBvdXRwdXQ/OiBOb2RlSlMuV3JpdGFibGVTdHJlYW07XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgQ2RrQ2xpT3B0aW9ucyBleHRlbmRzIFNoZWxsT3B0aW9ucyB7XG4gIG9wdGlvbnM/OiBzdHJpbmdbXTtcbiAgbmV2ZXJSZXF1aXJlQXBwcm92YWw/OiBib29sZWFuO1xuICB2ZXJib3NlPzogYm9vbGVhbjtcbn1cblxuLyoqXG4gKiBQcmVwYXJlIGEgdGFyZ2V0IGRpciBieXJlcGxpY2F0aW5nIGEgc291cmNlIGRpcmVjdG9yeVxuICovXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gY2xvbmVEaXJlY3Rvcnkoc291cmNlOiBzdHJpbmcsIHRhcmdldDogc3RyaW5nLCBvdXRwdXQ/OiBOb2RlSlMuV3JpdGFibGVTdHJlYW0pIHtcbiAgYXdhaXQgc2hlbGwoWydybScsICctcmYnLCB0YXJnZXRdLCB7IG91dHB1dCB9KTtcbiAgYXdhaXQgc2hlbGwoWydta2RpcicsICctcCcsIHRhcmdldF0sIHsgb3V0cHV0IH0pO1xuICBhd2FpdCBzaGVsbChbJ2NwJywgJy1SJywgc291cmNlICsgJy8qJywgdGFyZ2V0XSwgeyBvdXRwdXQgfSk7XG59XG5cbmV4cG9ydCBjbGFzcyBUZXN0Rml4dHVyZSB7XG4gIHB1YmxpYyByZWFkb25seSBxdWFsaWZpZXIgPSByYW5kb21TdHJpbmcoKS5zdWJzdHIoMCwgMTApO1xuICBwcml2YXRlIHJlYWRvbmx5IGJ1Y2tldHNUb0RlbGV0ZSA9IG5ldyBBcnJheTxzdHJpbmc+KCk7XG5cbiAgY29uc3RydWN0b3IoXG4gICAgcHVibGljIHJlYWRvbmx5IGludGVnVGVzdERpcjogc3RyaW5nLFxuICAgIHB1YmxpYyByZWFkb25seSBzdGFja05hbWVQcmVmaXg6IHN0cmluZyxcbiAgICBwdWJsaWMgcmVhZG9ubHkgb3V0cHV0OiBOb2RlSlMuV3JpdGFibGVTdHJlYW0sXG4gICAgcHVibGljIHJlYWRvbmx5IGF3czogQXdzQ2xpZW50cykge1xuICB9XG5cbiAgcHVibGljIGxvZyhzOiBzdHJpbmcpIHtcbiAgICB0aGlzLm91dHB1dC53cml0ZShgJHtzfVxcbmApO1xuICB9XG5cbiAgcHVibGljIGFzeW5jIHNoZWxsKGNvbW1hbmQ6IHN0cmluZ1tdLCBvcHRpb25zOiBPbWl0PFNoZWxsT3B0aW9ucywgJ2N3ZCd8J291dHB1dCc+ID0ge30pOiBQcm9taXNlPHN0cmluZz4ge1xuICAgIHJldHVybiBzaGVsbChjb21tYW5kLCB7XG4gICAgICBvdXRwdXQ6IHRoaXMub3V0cHV0LFxuICAgICAgY3dkOiB0aGlzLmludGVnVGVzdERpcixcbiAgICAgIC4uLm9wdGlvbnMsXG4gICAgfSk7XG4gIH1cblxuICBwdWJsaWMgYXN5bmMgY2RrRGVwbG95KHN0YWNrTmFtZXM6IHN0cmluZyB8IHN0cmluZ1tdLCBvcHRpb25zOiBDZGtDbGlPcHRpb25zID0ge30pIHtcbiAgICBzdGFja05hbWVzID0gdHlwZW9mIHN0YWNrTmFtZXMgPT09ICdzdHJpbmcnID8gW3N0YWNrTmFtZXNdIDogc3RhY2tOYW1lcztcblxuICAgIGNvbnN0IG5ldmVyUmVxdWlyZUFwcHJvdmFsID0gb3B0aW9ucy5uZXZlclJlcXVpcmVBcHByb3ZhbCA/PyB0cnVlO1xuXG4gICAgcmV0dXJuIHRoaXMuY2RrKFsnZGVwbG95JyxcbiAgICAgIC4uLihuZXZlclJlcXVpcmVBcHByb3ZhbCA/IFsnLS1yZXF1aXJlLWFwcHJvdmFsPW5ldmVyJ10gOiBbXSksIC8vIERlZmF1bHQgdG8gbm8gYXBwcm92YWwgaW4gYW4gdW5hdHRlbmRlZCB0ZXN0XG4gICAgICAuLi4ob3B0aW9ucy5vcHRpb25zID8/IFtdKSxcbiAgICAgIC4uLnRoaXMuZnVsbFN0YWNrTmFtZShzdGFja05hbWVzKV0sIG9wdGlvbnMpO1xuICB9XG5cbiAgcHVibGljIGFzeW5jIGNka0Rlc3Ryb3koc3RhY2tOYW1lczogc3RyaW5nIHwgc3RyaW5nW10sIG9wdGlvbnM6IENka0NsaU9wdGlvbnMgPSB7fSkge1xuICAgIHN0YWNrTmFtZXMgPSB0eXBlb2Ygc3RhY2tOYW1lcyA9PT0gJ3N0cmluZycgPyBbc3RhY2tOYW1lc10gOiBzdGFja05hbWVzO1xuXG4gICAgcmV0dXJuIHRoaXMuY2RrKFsnZGVzdHJveScsXG4gICAgICAnLWYnLCAvLyBXZSBuZXZlciB3YW50IGEgcHJvbXB0IGluIGFuIHVuYXR0ZW5kZWQgdGVzdFxuICAgICAgLi4uKG9wdGlvbnMub3B0aW9ucyA/PyBbXSksXG4gICAgICAuLi50aGlzLmZ1bGxTdGFja05hbWUoc3RhY2tOYW1lcyldLCBvcHRpb25zKTtcbiAgfVxuXG4gIHB1YmxpYyBhc3luYyBjZGsoYXJnczogc3RyaW5nW10sIG9wdGlvbnM6IENka0NsaU9wdGlvbnMgPSB7fSkge1xuICAgIGNvbnN0IHZlcmJvc2UgPSBvcHRpb25zLnZlcmJvc2UgPz8gdHJ1ZTtcblxuICAgIHJldHVybiB0aGlzLnNoZWxsKFsnY2RrJywgLi4uKHZlcmJvc2UgPyBbJy12J10gOiBbXSksIC4uLmFyZ3NdLCB7XG4gICAgICAuLi5vcHRpb25zLFxuICAgICAgbW9kRW52OiB7XG4gICAgICAgIEFXU19SRUdJT046IHRoaXMuYXdzLnJlZ2lvbixcbiAgICAgICAgQVdTX0RFRkFVTFRfUkVHSU9OOiB0aGlzLmF3cy5yZWdpb24sXG4gICAgICAgIFNUQUNLX05BTUVfUFJFRklYOiB0aGlzLnN0YWNrTmFtZVByZWZpeCxcbiAgICAgICAgLi4ub3B0aW9ucy5tb2RFbnYsXG4gICAgICB9LFxuICAgIH0pO1xuICB9XG5cbiAgcHVibGljIGZ1bGxTdGFja05hbWUoc3RhY2tOYW1lOiBzdHJpbmcpOiBzdHJpbmc7XG4gIHB1YmxpYyBmdWxsU3RhY2tOYW1lKHN0YWNrTmFtZXM6IHN0cmluZ1tdKTogc3RyaW5nW107XG4gIHB1YmxpYyBmdWxsU3RhY2tOYW1lKHN0YWNrTmFtZXM6IHN0cmluZyB8IHN0cmluZ1tdKTogc3RyaW5nIHwgc3RyaW5nW10ge1xuICAgIGlmICh0eXBlb2Ygc3RhY2tOYW1lcyA9PT0gJ3N0cmluZycpIHtcbiAgICAgIHJldHVybiBgJHt0aGlzLnN0YWNrTmFtZVByZWZpeH0tJHtzdGFja05hbWVzfWA7XG4gICAgfSBlbHNlIHtcbiAgICAgIHJldHVybiBzdGFja05hbWVzLm1hcChzID0+IGAke3RoaXMuc3RhY2tOYW1lUHJlZml4fS0ke3N9YCk7XG4gICAgfVxuICB9XG5cbiAgLyoqXG4gICAqIEFwcGVuZCB0aGlzIHRvIHRoZSBsaXN0IG9mIGJ1Y2tldHMgdG8gcG90ZW50aWFsbHkgZGVsZXRlXG4gICAqXG4gICAqIEF0IHRoZSBlbmQgb2YgYSB0ZXN0LCB3ZSBjbGVhbiB1cCBidWNrZXRzIHRoYXQgbWF5IG5vdCBoYXZlIGdvdHRlbiBkZXN0cm95ZWRcbiAgICogKGZvciB3aGF0ZXZlciByZWFzb24pLlxuICAgKi9cbiAgcHVibGljIHJlbWVtYmVyVG9EZWxldGVCdWNrZXQoYnVja2V0TmFtZTogc3RyaW5nKSB7XG4gICAgdGhpcy5idWNrZXRzVG9EZWxldGUucHVzaChidWNrZXROYW1lKTtcbiAgfVxuXG4gIC8qKlxuICAgKiBDbGVhbnVwIGxlZnRvdmVyIHN0YWNrcyBhbmQgYnVja2V0c1xuICAgKi9cbiAgcHVibGljIGFzeW5jIGRpc3Bvc2Uoc3VjY2VzczogYm9vbGVhbikge1xuICAgIGNvbnN0IHN0YWNrc1RvRGVsZXRlID0gYXdhaXQgdGhpcy5kZWxldGVhYmxlU3RhY2tzKHRoaXMuc3RhY2tOYW1lUHJlZml4KTtcblxuICAgIC8vIEJvb3RzdHJhcCBzdGFja3MgaGF2ZSBidWNrZXRzIHRoYXQgbmVlZCB0byBiZSBjbGVhbmVkXG4gICAgY29uc3QgYnVja2V0TmFtZXMgPSBzdGFja3NUb0RlbGV0ZS5tYXAoc3RhY2sgPT4gb3V0cHV0RnJvbVN0YWNrKCdCdWNrZXROYW1lJywgc3RhY2spKS5maWx0ZXIoZGVmaW5lZCk7XG4gICAgYXdhaXQgUHJvbWlzZS5hbGwoYnVja2V0TmFtZXMubWFwKGIgPT4gdGhpcy5hd3MuZW1wdHlCdWNrZXQoYikpKTtcblxuICAgIC8vIEJvb3RzdHJhcCBzdGFja3MgaGF2ZSBFQ1IgcmVwb3NpdG9yaWVzIHdpdGggaW1hZ2VzIHdoaWNoIHNob3VsZCBiZSBkZWxldGVkXG4gICAgY29uc3QgaW1hZ2VSZXBvc2l0b3J5TmFtZXMgPSBzdGFja3NUb0RlbGV0ZS5tYXAoc3RhY2sgPT4gb3V0cHV0RnJvbVN0YWNrKCdJbWFnZVJlcG9zaXRvcnlOYW1lJywgc3RhY2spKS5maWx0ZXIoZGVmaW5lZCk7XG4gICAgYXdhaXQgUHJvbWlzZS5hbGwoaW1hZ2VSZXBvc2l0b3J5TmFtZXMubWFwKHIgPT4gdGhpcy5hd3MuZGVsZXRlSW1hZ2VSZXBvc2l0b3J5KHIpKSk7XG5cbiAgICBhd2FpdCB0aGlzLmF3cy5kZWxldGVTdGFja3MoLi4uc3RhY2tzVG9EZWxldGUubWFwKHMgPT4gcy5TdGFja05hbWUpKTtcblxuICAgIC8vIFdlIG1pZ2h0IGhhdmUgbGVha2VkIHNvbWUgYnVja2V0cyBieSB1cGdyYWRpbmcgdGhlIGJvb3RzdHJhcCBzdGFjay4gQmVcbiAgICAvLyBzdXJlIHRvIGNsZWFuIGV2ZXJ5dGhpbmcuXG4gICAgZm9yIChjb25zdCBidWNrZXQgb2YgdGhpcy5idWNrZXRzVG9EZWxldGUpIHtcbiAgICAgIGF3YWl0IHRoaXMuYXdzLmRlbGV0ZUJ1Y2tldChidWNrZXQpO1xuICAgIH1cblxuICAgIC8vIElmIHRoZSB0ZXN0cyBjb21wbGV0ZWQgc3VjY2Vzc2Z1bGx5LCBoYXBwaWx5IGRlbGV0ZSB0aGUgZml4dHVyZVxuICAgIC8vIChvdGhlcndpc2UgbGVhdmUgaXQgZm9yIGh1bWFucyB0byBpbnNwZWN0KVxuICAgIGlmIChzdWNjZXNzKSB7XG4gICAgICByaW1yYWYodGhpcy5pbnRlZ1Rlc3REaXIpO1xuICAgIH1cbiAgfVxuXG4gIC8qKlxuICAgKiBSZXR1cm4gdGhlIHN0YWNrcyBzdGFydGluZyB3aXRoIG91ciB0ZXN0aW5nIHByZWZpeCB0aGF0IHNob3VsZCBiZSBkZWxldGVkXG4gICAqL1xuICBwcml2YXRlIGFzeW5jIGRlbGV0ZWFibGVTdGFja3MocHJlZml4OiBzdHJpbmcpOiBQcm9taXNlPEFXUy5DbG91ZEZvcm1hdGlvbi5TdGFja1tdPiB7XG4gICAgY29uc3Qgc3RhdHVzRmlsdGVyID0gW1xuICAgICAgJ0NSRUFURV9JTl9QUk9HUkVTUycsICdDUkVBVEVfRkFJTEVEJywgJ0NSRUFURV9DT01QTEVURScsXG4gICAgICAnUk9MTEJBQ0tfSU5fUFJPR1JFU1MnLCAnUk9MTEJBQ0tfRkFJTEVEJywgJ1JPTExCQUNLX0NPTVBMRVRFJyxcbiAgICAgICdERUxFVEVfRkFJTEVEJyxcbiAgICAgICdVUERBVEVfSU5fUFJPR1JFU1MnLCAnVVBEQVRFX0NPTVBMRVRFX0NMRUFOVVBfSU5fUFJPR1JFU1MnLFxuICAgICAgJ1VQREFURV9DT01QTEVURScsICdVUERBVEVfUk9MTEJBQ0tfSU5fUFJPR1JFU1MnLFxuICAgICAgJ1VQREFURV9ST0xMQkFDS19GQUlMRUQnLFxuICAgICAgJ1VQREFURV9ST0xMQkFDS19DT01QTEVURV9DTEVBTlVQX0lOX1BST0dSRVNTJyxcbiAgICAgICdVUERBVEVfUk9MTEJBQ0tfQ09NUExFVEUnLCAnUkVWSUVXX0lOX1BST0dSRVNTJyxcbiAgICAgICdJTVBPUlRfSU5fUFJPR1JFU1MnLCAnSU1QT1JUX0NPTVBMRVRFJyxcbiAgICAgICdJTVBPUlRfUk9MTEJBQ0tfSU5fUFJPR1JFU1MnLCAnSU1QT1JUX1JPTExCQUNLX0ZBSUxFRCcsXG4gICAgICAnSU1QT1JUX1JPTExCQUNLX0NPTVBMRVRFJyxcbiAgICBdO1xuXG4gICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCB0aGlzLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7fSk7XG5cbiAgICByZXR1cm4gKHJlc3BvbnNlLlN0YWNrcyA/PyBbXSlcbiAgICAgIC5maWx0ZXIocyA9PiBzLlN0YWNrTmFtZS5zdGFydHNXaXRoKHByZWZpeCkpXG4gICAgICAuZmlsdGVyKHMgPT4gc3RhdHVzRmlsdGVyLmluY2x1ZGVzKHMuU3RhY2tTdGF0dXMpKVxuICAgICAgLmZpbHRlcihzID0+IHMuUm9vdElkID09PSB1bmRlZmluZWQpOyAvLyBPbmx5IGRlbGV0ZSBwYXJlbnQgc3RhY2tzLiBOZXN0ZWQgc3RhY2tzIGFyZSBkZWxldGVkIGluIHRoZSBwcm9jZXNzXG4gIH1cbn1cblxuLyoqXG4gKiBQZXJmb3JtIGEgb25lLXRpbWUgcXVpY2sgc2FuaXR5IGNoZWNrIHRoYXQgdGhlIEFXUyBjbGllbnRzIGhhcyBwcm9wZXJseSBjb25maWd1cmVkIGNyZWRlbnRpYWxzXG4gKlxuICogSWYgd2UgZG9uJ3QgZG8gdGhpcywgY2FsbHMgYXJlIGdvaW5nIHRvIGZhaWwgYW5kIHRoZXknbGwgYmUgcmV0cmllZCBhbmQgZXZlcnl0aGluZyB3aWxsIHRha2VcbiAqIGZvcmV2ZXIgYmVmb3JlIHRoZSB1c2VyIG5vdGljZXMgYSBzaW1wbGUgbWlzY29uZmlndXJhdGlvbi5cbiAqXG4gKiBXZSBjYW4ndCBjaGVjayBmb3IgdGhlIHByZXNlbmNlIG9mIGVudmlyb25tZW50IHZhcmlhYmxlcyBzaW5jZSBjcmVkZW50aWFscyBjb3VsZCBjb21lIGZyb21cbiAqIGFueXdoZXJlLCBzbyBkbyBzaW1wbGUgYWNjb3VudCByZXRyaWV2YWwuXG4gKlxuICogT25seSBkbyBpdCBvbmNlIHBlciBwcm9jZXNzLlxuICovXG5hc3luYyBmdW5jdGlvbiBzYW5pdHlDaGVjayhhd3M6IEF3c0NsaWVudHMpIHtcbiAgaWYgKHNhbml0eUNoZWNrZWQgPT09IHVuZGVmaW5lZCkge1xuICAgIHRyeSB7XG4gICAgICBhd2FpdCBhd3MuYWNjb3VudCgpO1xuICAgICAgc2FuaXR5Q2hlY2tlZCA9IHRydWU7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgc2FuaXR5Q2hlY2tlZCA9IGZhbHNlO1xuICAgICAgdGhyb3cgbmV3IEVycm9yKGBBV1MgY3JlZGVudGlhbHMgcHJvYmFibHkgbm90IGNvbmZpZ3VyZWQsIGdvdCBlcnJvcjogJHtlLm1lc3NhZ2V9YCk7XG4gICAgfVxuICB9XG4gIGlmICghc2FuaXR5Q2hlY2tlZCkge1xuICAgIHRocm93IG5ldyBFcnJvcignQVdTIGNyZWRlbnRpYWxzIHByb2JhYmx5IG5vdCBjb25maWd1cmVkLCBzZWUgcHJldmlvdXMgZXJyb3InKTtcbiAgfVxufVxubGV0IHNhbml0eUNoZWNrZWQ6IGJvb2xlYW4gfCB1bmRlZmluZWQ7XG5cbi8qKlxuICogTWFrZSBzdXJlIHRoYXQgdGhlIGdpdmVuIGVudmlyb25tZW50IGlzIGJvb3RzdHJhcHBlZFxuICpcbiAqIFNpbmNlIHdlIGdvIHN0cmlwaW5nIGFjcm9zcyByZWdpb25zLCBpdCdzIGdvaW5nIHRvIHN1Y2sgZG9pbmcgdGhpc1xuICogYnkgaGFuZCBzbyBsZXQncyBqdXN0IG1hc3MtYXV0b21hdGUgaXQuXG4gKi9cbmFzeW5jIGZ1bmN0aW9uIGVuc3VyZUJvb3RzdHJhcHBlZChmaXh0dXJlOiBUZXN0Rml4dHVyZSkge1xuICAvLyBPbGQtc3R5bGUgYm9vdHN0cmFwIHN0YWNrIHdpdGggZGVmYXVsdCBuYW1lXG4gIGlmIChhd2FpdCBmaXh0dXJlLmF3cy5zdGFja1N0YXR1cygnQ0RLVG9vbGtpdCcpID09PSB1bmRlZmluZWQpIHtcbiAgICBhd2FpdCBmaXh0dXJlLmNkayhbJ2Jvb3RzdHJhcCcsIGBhd3M6Ly8ke2F3YWl0IGZpeHR1cmUuYXdzLmFjY291bnQoKX0vJHtmaXh0dXJlLmF3cy5yZWdpb259YF0pO1xuICB9XG59XG5cbi8qKlxuICogQSBzaGVsbCBjb21tYW5kIHRoYXQgZG9lcyB3aGF0IHlvdSB3YW50XG4gKlxuICogSXMgcGxhdGZvcm0tYXdhcmUsIGhhbmRsZXMgZXJyb3JzIG5pY2VseS5cbiAqL1xuZXhwb3J0IGFzeW5jIGZ1bmN0aW9uIHNoZWxsKGNvbW1hbmQ6IHN0cmluZ1tdLCBvcHRpb25zOiBTaGVsbE9wdGlvbnMgPSB7fSk6IFByb21pc2U8c3RyaW5nPiB7XG4gIGlmIChvcHRpb25zLm1vZEVudiAmJiBvcHRpb25zLmVudikge1xuICAgIHRocm93IG5ldyBFcnJvcignVXNlIGVpdGhlciBlbnYgb3IgbW9kRW52IGJ1dCBub3QgYm90aCcpO1xuICB9XG5cbiAgb3B0aW9ucy5vdXRwdXQ/LndyaXRlKGDwn5K7ICR7Y29tbWFuZC5qb2luKCcgJyl9XFxuYCk7XG5cbiAgY29uc3QgZW52ID0gb3B0aW9ucy5lbnYgPz8gKG9wdGlvbnMubW9kRW52ID8geyAuLi5wcm9jZXNzLmVudiwgLi4ub3B0aW9ucy5tb2RFbnYgfSA6IHVuZGVmaW5lZCk7XG5cbiAgY29uc3QgY2hpbGQgPSBjaGlsZF9wcm9jZXNzLnNwYXduKGNvbW1hbmRbMF0sIGNvbW1hbmQuc2xpY2UoMSksIHtcbiAgICAuLi5vcHRpb25zLFxuICAgIGVudixcbiAgICAvLyBOZWVkIHRoaXMgZm9yIFdpbmRvd3Mgd2hlcmUgd2Ugd2FudCAuY21kIGFuZCAuYmF0IHRvIGJlIGZvdW5kIGFzIHdlbGwuXG4gICAgc2hlbGw6IHRydWUsXG4gICAgc3RkaW86IFsnaWdub3JlJywgJ3BpcGUnLCAncGlwZSddLFxuICB9KTtcblxuICByZXR1cm4gbmV3IFByb21pc2U8c3RyaW5nPigocmVzb2x2ZSwgcmVqZWN0KSA9PiB7XG4gICAgY29uc3Qgc3Rkb3V0ID0gbmV3IEFycmF5PEJ1ZmZlcj4oKTtcbiAgICBjb25zdCBzdGRlcnIgPSBuZXcgQXJyYXk8QnVmZmVyPigpO1xuXG4gICAgY2hpbGQuc3Rkb3V0IS5vbignZGF0YScsIGNodW5rID0+IHtcbiAgICAgIG9wdGlvbnMub3V0cHV0Py53cml0ZShjaHVuayk7XG4gICAgICBzdGRvdXQucHVzaChjaHVuayk7XG4gICAgfSk7XG5cbiAgICBjaGlsZC5zdGRlcnIhLm9uKCdkYXRhJywgY2h1bmsgPT4ge1xuICAgICAgb3B0aW9ucy5vdXRwdXQ/LndyaXRlKGNodW5rKTtcbiAgICAgIGlmIChvcHRpb25zLmNhcHR1cmVTdGRlcnIgPz8gdHJ1ZSkge1xuICAgICAgICBzdGRlcnIucHVzaChjaHVuayk7XG4gICAgICB9XG4gICAgfSk7XG5cbiAgICBjaGlsZC5vbmNlKCdlcnJvcicsIHJlamVjdCk7XG5cbiAgICBjaGlsZC5vbmNlKCdjbG9zZScsIGNvZGUgPT4ge1xuICAgICAgaWYgKGNvZGUgPT09IDAgfHwgb3B0aW9ucy5hbGxvd0VyckV4aXQpIHtcbiAgICAgICAgcmVzb2x2ZSgoQnVmZmVyLmNvbmNhdChzdGRvdXQpLnRvU3RyaW5nKCd1dGYtOCcpICsgQnVmZmVyLmNvbmNhdChzdGRlcnIpLnRvU3RyaW5nKCd1dGYtOCcpKS50cmltKCkpO1xuICAgICAgfSBlbHNlIHtcbiAgICAgICAgcmVqZWN0KG5ldyBFcnJvcihgJyR7Y29tbWFuZC5qb2luKCcgJyl9JyBleGl0ZWQgd2l0aCBlcnJvciBjb2RlICR7Y29kZX1gKSk7XG4gICAgICB9XG4gICAgfSk7XG4gIH0pO1xufVxuXG5mdW5jdGlvbiBkZWZpbmVkPEE+KHg6IEEpOiB4IGlzIE5vbk51bGxhYmxlPEE+IHtcbiAgcmV0dXJuIHggIT09IHVuZGVmaW5lZDtcbn1cblxuLyoqXG4gKiBybSAtcmYgcmVpbXBsZW1lbnRhdGlvbiwgZG9uJ3Qgd2FudCB0byBkZXBlbmQgb24gYW4gTlBNIHBhY2thZ2UgZm9yIHRoaXNcbiAqL1xuZXhwb3J0IGZ1bmN0aW9uIHJpbXJhZihmc1BhdGg6IHN0cmluZykge1xuICB0cnkge1xuICAgIGNvbnN0IGlzRGlyID0gZnMubHN0YXRTeW5jKGZzUGF0aCkuaXNEaXJlY3RvcnkoKTtcblxuICAgIGlmIChpc0Rpcikge1xuICAgICAgZm9yIChjb25zdCBmaWxlIG9mIGZzLnJlYWRkaXJTeW5jKGZzUGF0aCkpIHtcbiAgICAgICAgcmltcmFmKHBhdGguam9pbihmc1BhdGgsIGZpbGUpKTtcbiAgICAgIH1cbiAgICAgIGZzLnJtZGlyU3luYyhmc1BhdGgpO1xuICAgIH0gZWxzZSB7XG4gICAgICBmcy51bmxpbmtTeW5jKGZzUGF0aCk7XG4gICAgfVxuICB9IGNhdGNoIChlKSB7XG4gICAgLy8gV2Ugd2lsbCBzdXJ2aXZlIEVOT0VOVFxuICAgIGlmIChlLmNvZGUgIT09ICdFTk9FTlQnKSB7IHRocm93IGU7IH1cbiAgfVxufVxuXG5leHBvcnQgZnVuY3Rpb24gcmFuZG9tU3RyaW5nKCkge1xuICAvLyBDcmF6eVxuICByZXR1cm4gTWF0aC5yYW5kb20oKS50b1N0cmluZygzNikucmVwbGFjZSgvW15hLXowLTldKy9nLCAnJyk7XG59Il19 \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.130.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.130.0/NOTES.md new file mode 100644 index 000000000..d2b0b6c31 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.130.0/NOTES.md @@ -0,0 +1 @@ +This patch brings the [fix](https://github.com/aws/aws-cdk/pull/29305) into the regression suite. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.130.0/skip-tests.txt b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.130.0/skip-tests.txt new file mode 100644 index 000000000..4271078bd --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.130.0/skip-tests.txt @@ -0,0 +1,5 @@ +# The fix for upgrading lambda runtimes from python 3.7 to 3.12 is not relased yet: https://github.com/aws/aws-cdk/pull/29305 +# Skipping the bootstrapping test as it is flakey; only doing so to get the 2.131.0 release out this week. + +sam can locally test the synthesized cdk application +switch on termination protection, switch is left alone on re-bootstrap \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.132.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.132.0/NOTES.md new file mode 100644 index 000000000..6f59e64a3 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.132.0/NOTES.md @@ -0,0 +1 @@ +This patch brings the [fix](https://github.com/aws/aws-cdk/issues/29420) into the regression suite. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.132.0/skip-tests.txt b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.132.0/skip-tests.txt new file mode 100644 index 000000000..744f3c339 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.132.0/skip-tests.txt @@ -0,0 +1,4 @@ +# Skipping the test to fix issue https://github.com/aws/aws-cdk/issues/29420. +# cli-integ tests failing for the old tests with the new cli changes for list stacks. + +cdk ls --show-dependencies --json \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.142.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.142.0/NOTES.md new file mode 100644 index 000000000..a606d9cd2 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.142.0/NOTES.md @@ -0,0 +1 @@ +This patch brings the [fix](https://github.com/aws/aws-cdk/issues/30241) into the regression suite. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.142.0/skip-tests.txt b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.142.0/skip-tests.txt new file mode 100644 index 000000000..0351c9fb9 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.142.0/skip-tests.txt @@ -0,0 +1,4 @@ +# Skipping the test to fix issue https://github.com/aws/aws-cdk/issues/30241. +# cli-integ tests failing for the old tests with the new cli changes for diff stacks. + +cdk diff picks up changes that are only present in changeset \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.160.0/skip-tests.txt b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.160.0/skip-tests.txt new file mode 100644 index 000000000..3f514816d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.160.0/skip-tests.txt @@ -0,0 +1,2 @@ +# This test asserts too much about the output of the CLI. +security related changes without a CLI are expected to fail when approval is required \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.161.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.161.0/NOTES.md new file mode 100644 index 000000000..8e21b44da --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.161.0/NOTES.md @@ -0,0 +1 @@ +This patch brings the [fix](https://github.com/aws/aws-cdk/issues/31654) into the regression suite. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.161.0/skip-tests.txt b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.161.0/skip-tests.txt new file mode 100644 index 000000000..f681a6407 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.161.0/skip-tests.txt @@ -0,0 +1,5 @@ +# Skipping the test to fix issue https://github.com/aws/aws-cdk/issues/31654. +# cli-integ tests failing for the old tests with the new cli changes for nested stacks. + +test cdk rollback +test cdk rollback --force \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.166.0/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.166.0/NOTES.md new file mode 100644 index 000000000..7eea89064 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.166.0/NOTES.md @@ -0,0 +1 @@ +The output of this test is changed by the sdk upgrade. The type and content of the error have changed from sdkv2 to sdkv3. We now receive more specific information about the error type. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.166.0/skip-tests.txt b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.166.0/skip-tests.txt new file mode 100644 index 000000000..7dd73a4a1 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.166.0/skip-tests.txt @@ -0,0 +1,2 @@ +# Skipping test due to the incompatibility in error output between sdkv2 and sdkv3 +hotswap deployment for ecs service detects failed deployment and errors \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.178.1/NOTES.md b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.178.1/NOTES.md new file mode 100644 index 000000000..1aedc7830 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.178.1/NOTES.md @@ -0,0 +1,2 @@ +This test is failing on what seems to be an upstream or dependency issue. +Skipping it because it prevents us from a release a patch. diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.178.1/skip-tests.txt b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.178.1/skip-tests.txt new file mode 100644 index 000000000..4d3acd6ce --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cli-regression-patches/v2.178.1/skip-tests.txt @@ -0,0 +1 @@ +sam can locally test the synthesized cdk application \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/0.36.0/InitStack.template.json b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/0.36.0/InitStack.template.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/0.36.0/InitStack.template.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/0.36.0/cdk.out b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/0.36.0/cdk.out new file mode 100644 index 000000000..bfe018f23 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/0.36.0/cdk.out @@ -0,0 +1 @@ +{"version":"0.36.0"} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/0.36.0/manifest.json b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/0.36.0/manifest.json new file mode 100644 index 000000000..025294fe0 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/0.36.0/manifest.json @@ -0,0 +1,19 @@ +{ + "version": "0.36.0", + "artifacts": { + "InitStack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "InitStack.template.json" + } + } + }, + "runtime": { + "libraries": { + "@aws-cdk/core": "1.12.0", + "@aws-cdk/cx-api": "1.12.0", + "jsii-runtime": "node.js/v8.11.4" + } + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-lookup-default-vpc/InitStack.template.json b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-lookup-default-vpc/InitStack.template.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-lookup-default-vpc/InitStack.template.json @@ -0,0 +1,2 @@ +{ +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-lookup-default-vpc/cdk.out b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-lookup-default-vpc/cdk.out new file mode 100644 index 000000000..77925ad44 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-lookup-default-vpc/cdk.out @@ -0,0 +1 @@ +{"version":"1.10.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-lookup-default-vpc/manifest.json.js b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-lookup-default-vpc/manifest.json.js new file mode 100644 index 000000000..e328ea2e9 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-lookup-default-vpc/manifest.json.js @@ -0,0 +1,37 @@ +process.stdout.write(JSON.stringify({ + "version": "1.10.0", + "artifacts": { + "InitStack": { + "type": "aws:cloudformation:stack", + "environment": `aws://${process.env.TEST_ACCOUNT}/${process.env.TEST_REGION}`, + "properties": { + "templateFile": "InitStack.template.json" + } + } + }, + "runtime": { + "libraries": { + "@aws-cdk/core": "1.14.0", + "@aws-cdk/cx-api": "1.14.0", + "@aws-cdk/aws-ec2": "1.14.0", + "@aws-cdk/aws-iam": "1.14.0", + "@aws-cdk/region-info": "1.14.0", + "@aws-cdk/aws-ssm": "1.14.0", + "@aws-cdk/aws-cloudwatch": "1.14.0", + "jsii-runtime": "node.js/v8.11.4" + } + }, + "missing": [ + { + "key": `vpc-provider:account=${process.env.TEST_ACCOUNT}:filter.isDefault=true:region=${process.env.TEST_REGION}`, + "props": { + "account": process.env.TEST_ACCOUNT, + "region": process.env.TEST_REGION, + "filter": { + "isDefault": "true" + } + }, + "provider": "vpc-provider" + } + ] +}, undefined, 2)); diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-request-azs/InitStack.template.json b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-request-azs/InitStack.template.json new file mode 100644 index 000000000..2c63c0851 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-request-azs/InitStack.template.json @@ -0,0 +1,2 @@ +{ +} diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-request-azs/cdk.out b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-request-azs/cdk.out new file mode 100644 index 000000000..77925ad44 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-request-azs/cdk.out @@ -0,0 +1 @@ +{"version":"1.10.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-request-azs/manifest.json.js b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-request-azs/manifest.json.js new file mode 100644 index 000000000..9c88f8ad7 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cloud-assemblies/1.10.0-request-azs/manifest.json.js @@ -0,0 +1,34 @@ +process.stdout.write(JSON.stringify({ + "version": "1.10.0", + "artifacts": { + "InitStack": { + "type": "aws:cloudformation:stack", + "environment": `aws://${process.env.TEST_ACCOUNT}/${process.env.TEST_REGION}`, + "properties": { + "templateFile": "InitStack.template.json" + } + } + }, + "runtime": { + "libraries": { + "@aws-cdk/core": "1.14.0", + "@aws-cdk/cx-api": "1.14.0", + "@aws-cdk/aws-ec2": "1.14.0", + "@aws-cdk/aws-iam": "1.14.0", + "@aws-cdk/region-info": "1.14.0", + "@aws-cdk/aws-ssm": "1.14.0", + "@aws-cdk/aws-cloudwatch": "1.14.0", + "jsii-runtime": "node.js/v8.11.4" + } + }, + "missing": [ + { + "key": `availability-zones:account=${process.env.TEST_ACCOUNT}:region=${process.env.TEST_REGION}`, + "props": { + "account": process.env.TEST_ACCOUNT, + "region": process.env.TEST_REGION, + }, + "provider": "availability-zones" + } + ] +}, undefined, 2)); diff --git a/packages/@aws-cdk-testing/cli-integ/resources/integ.jest.config.js b/packages/@aws-cdk-testing/cli-integ/resources/integ.jest.config.js new file mode 100644 index 000000000..2b1f1add0 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/integ.jest.config.js @@ -0,0 +1,27 @@ +const path = require('path'); + +const rootDir = path.resolve(__dirname, '..', 'tests', process.env.TEST_SUITE_NAME); + +if (rootDir.includes('node_modules')) { + // Jest < 28 under no circumstances supports loading test if there's node_modules anywhere in the path, + // and Jest >= 28 requires a newer TypeScript version than the one we support. + throw new Error(`This package must not be 'npm install'ed (found node_modules in dir: ${rootDir})`); +} + +module.exports = { + rootDir, + testMatch: [`**/*.integtest.js`], + moduleFileExtensions: ["js"], + + testEnvironment: "node", + + // Because of the way Jest concurrency works, this timeout includes waiting + // for the lock. Which is almost never what we actually care about. Set it high. + testTimeout: 2 * 60 * 60_000, + + maxWorkers: 50, + reporters: [ + "default", + ["jest-junit", { suiteName: "jest tests", outputDirectory: "coverage" }] + ] +}; diff --git a/packages/@aws-cdk-testing/cli-integ/resources/templates/sqs-template.json b/packages/@aws-cdk-testing/cli-integ/resources/templates/sqs-template.json new file mode 100644 index 000000000..57c9ef5d1 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/templates/sqs-template.json @@ -0,0 +1,36 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "AWS CloudFormation Sample Template SQS_With_CloudWatch_Alarms: Sample template showing how to create an SQS queue with AWS CloudWatch alarms on queue depth.", + "Resources": { + "MyQueue": { + "Type": "AWS::SQS::Queue", + "Properties": {} + } + }, + "Outputs": { + "QueueURL": { + "Description": "URL of newly created SQS Queue", + "Value": { + "Ref": "MyQueue" + } + }, + "QueueARN": { + "Description": "ARN of newly created SQS Queue", + "Value": { + "Fn::GetAtt": [ + "MyQueue", + "Arn" + ] + } + }, + "QueueName": { + "Description": "Name newly created SQS Queue", + "Value": { + "Fn::GetAtt": [ + "MyQueue", + "QueueName" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/skip-tests.txt b/packages/@aws-cdk-testing/cli-integ/skip-tests.txt new file mode 100644 index 000000000..e48344380 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/skip-tests.txt @@ -0,0 +1,8 @@ +# This file is empty on purpose. Leave it here as documentation +# and an example. +# +# Copy this file to resources/cli-regression-patches/vX.Y.Z/skip-tests.txt +# and edit it there if you want to exclude certain tests from running +# when performing a certain version's regression tests. +# +# Put a test name on a line by itself to skip it. \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/test-reports/junit.xml b/packages/@aws-cdk-testing/cli-integ/test-reports/junit.xml new file mode 100644 index 000000000..d1dd8fc5b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/test-reports/junit.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/test/resource-pool.test.ts b/packages/@aws-cdk-testing/cli-integ/test/resource-pool.test.ts new file mode 100644 index 000000000..ff5cf3c43 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/test/resource-pool.test.ts @@ -0,0 +1,92 @@ +import { sleep } from '../lib'; +import { ResourcePool } from '../lib/resource-pool'; + +jest.setTimeout(30_000); + +const POOL_NAME = 'resource-pool.test'; + +test('take and dispose', async () => { + const pool = ResourcePool.withResources(POOL_NAME, ['a']); + const take1 = pool.take(); + + let released = false; + + const lease1 = await take1; + + // We must start the take2 only after take1 has definitely + // succeeded, otherwise we have a race condition if take2 happens to + // win the race (we expect take1 to succeed and take2 to wait). + const take2 = pool.take(); + + // awaiting 'take2' would now block but we add an async + // handler to it to flip a boolean to see when it gets activated. + void(take2.then(() => released = true)); + + expect(lease1.value).toEqual('a'); + await waitTick(); + expect(released).toEqual(false); + + await lease1.dispose(); + await waitTick(); // This works because setImmediate is scheduled in LIFO order + + const lease2 = await take2; + await lease2.dispose(); + expect(released).toEqual(true); +}); + +test('double dispose throws', async () => { + const pool = ResourcePool.withResources(POOL_NAME, ['a']); + const lease = await pool.take(); + + await lease.dispose(); + await expect(() => lease.dispose()).rejects.toThrow(); +}); + +test('somewhat balance', async () => { + const counters = { + a: 0, + b: 0, + c: 0, + d: 0, + e: 0, + }; + const N = 100; + let maxConcurrency = 0; + let concurrency = 0; + + const keys = Object.keys(counters) as Array ; + const pool = ResourcePool.withResources(POOL_NAME, keys); + // eslint-disable-next-line @cdklabs/promiseall-no-unbounded-parallelism + await Promise.all(Array.from(range(N)).map(() => + pool.using(async (x) => { + counters[x] += 1; + concurrency += 1; + maxConcurrency = Math.max(maxConcurrency, concurrency); + try { + await sleep(10); + } finally { + concurrency -= 1; + } + }), + )); + + // Regardless of which resource(s) we used, the total count should add up to N + const sum = Object.values(counters).reduce((a, b) => a + b, 0); + expect(sum).toEqual(N); + // There was concurrency + expect(maxConcurrency).toBeGreaterThan(2); + // All counters are used + for (const count of Object.values(counters)) { + expect(count).toBeGreaterThan(0); + } +}); + +function waitTick() { + return new Promise(setImmediate); +} + +function* range(n: number) { + for (let i = 0; i < n; i++) { + yield i; + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/test/xpmutex.test.ts b/packages/@aws-cdk-testing/cli-integ/test/xpmutex.test.ts new file mode 100644 index 000000000..73e0d9140 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/test/xpmutex.test.ts @@ -0,0 +1,45 @@ +import { XpMutexPool } from '../lib/xpmutex'; + +const POOL = XpMutexPool.fromName('test-pool'); + +test('acquire waits', async () => { + const mux = POOL.mutex('testA'); + let secondLockAcquired = false; + + // Current "process" acquires lock + const lock = await mux.acquire(); + + // Start a second "process" that tries to acquire the lock + const secondProcess = (async () => { + const secondLock = await mux.acquire(); + try { + secondLockAcquired = true; + } finally { + await secondLock.release(); + } + })(); + + // Once we release the lock the second process is free to take it + expect(secondLockAcquired).toBe(false); + await lock.release(); + + // We expect the variable to become true + await waitFor(() => secondLockAcquired); + expect(secondLockAcquired).toBe(true); + + await secondProcess; +}); + +/** + * Poll for some condition every 10ms + */ +function waitFor(pred: () => boolean): Promise { + return new Promise((ok) => { + const timerHandle = setInterval(() => { + if (pred()) { + clearInterval(timerHandle); + ok(); + } + }, 5); + }); +} diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/README.md b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/README.md new file mode 100644 index 000000000..a26666bff --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/README.md @@ -0,0 +1,47 @@ +# CDK CLI Integration Tests + +These tests require AWS credentials, and exercise various aspects of the +CLI on a simple JavaScript CDK app (stored in `app/`). + +## Entry point + +``` +../run-against-repo ./test.sh +``` + +Running against a failing dist build: + +``` +# Download and unpack the zip +.../CMkBR4V$ tar xzvf js/aws-cdk-[0-9]*.tgz +.../CMkBR4V$ package/test/integ/run-against-dist package/test/integ/cli/test.sh +``` + +## Adding tests + +Even though tests are now written in TypeScript, this does not +conceptually change their SUT! They are still testing the CLI via +running it as a subprocess, they are NOT reaching directly into the CLI +code to test its private methods. It's just that setup/teardown/error +handling/assertions/AWS calls are much more convenient to do in a real +programming language. + +Compilation of the tests is done as part of the normal package build, at +which point it is using the dependencies brought in by the containing +`aws-cdk` package's `package.json`. + +When run in a non-development repo (as done during integ tests or canary runs), +the required dependencies are brought in just-in-time via `test.sh`. Any +new dependencies added for the tests should be added there as well. But, better +yet, don't add any dependencies at all. You shouldn't need to, these tests +are simple. + +## Configuration + +AWS credentials must be configured. + +Optional configuration: + +* `AWS_DEFAULT_REGION`, what region to deploy the stacks in. +* `IS_CANARY`, true or false. Affects the default stack name prefix to make + integration test and canary runs unique. diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk---exclusively-selects-only-selected-stack.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk---exclusively-selects-only-selected-stack.integtest.ts new file mode 100644 index 000000000..6ab31a612 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk---exclusively-selects-only-selected-stack.integtest.ts @@ -0,0 +1,29 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + '--exclusively selects only selected stack', + withDefaultFixture(async (fixture) => { + // Deploy the "depends-on-failed" stack, with --exclusively. It will NOT fail (because + // of --exclusively) and it WILL create an output we can check for to confirm that it did + // get deployed. + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs.mkdir(path.dirname(outputsFile), { recursive: true }); + + await fixture.cdkDeploy('depends-on-failed', { + options: ['--exclusively', '--outputs-file', outputsFile], + }); + + // Verify the output to see that the stack deployed + const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + expect(outputs).toEqual({ + [`${fixture.stackNamePrefix}-depends-on-failed`]: { + TopicName: `${fixture.stackNamePrefix}-depends-on-failedMyTopic`, + }, + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-assets/cdk-assets-uses-profile.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-assets/cdk-assets-uses-profile.integtest.ts new file mode 100644 index 000000000..cc2edb97b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-assets/cdk-assets-uses-profile.integtest.ts @@ -0,0 +1,90 @@ + +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('cdk-assets uses profile when specified', withDefaultFixture(async (fixture) => { + const currentCreds = await fixture.aws.credentials(); + + await fixture.shell(['npm', 'init', '-y']); + await fixture.shell(['npm', 'install', 'cdk-assets@latest']); + + const account = await fixture.aws.account(); + const region = fixture.aws.region; + const bucketName = `cdk-hnb659fds-assets-${account}-${region}`; + + // Write some asset files. Its important to have more than 1 because cdk-assets + // code has some funky state mutations that happens on each asset publishing. + const assetFile1 = 'testfile.txt'; + const assetFile2 = 'testfile.txt'; + await fs.writeFile(path.join(fixture.integTestDir, assetFile1), 'some asset file'); + await fs.writeFile(path.join(fixture.integTestDir, assetFile2), 'some asset file'); + + // Write an asset JSON file to publish to the bootstrapped environment + const assetsJson = { + version: '38.0.1', + files: { + testfile1: { + source: { + path: assetFile1, + packaging: 'file', + }, + destinations: { + current: { + region, + assumeRoleArn: `arn:\${AWS::Partition}:iam::${account}:role/cdk-hnb659fds-file-publishing-role-${account}-${region}`, + bucketName, + objectKey: `test-file1-${Date.now()}.json`, + }, + }, + }, + testfile2: { + source: { + path: assetFile2, + packaging: 'file', + }, + destinations: { + current: { + region, + assumeRoleArn: `arn:\${AWS::Partition}:iam::${account}:role/cdk-hnb659fds-file-publishing-role-${account}-${region}`, + bucketName, + objectKey: `test-file2-${Date.now()}.json`, + }, + }, + }, + }, + }; + + // create a profile with our current credentials. + // + // if you're wondering why can't we do the reverse (i.e write a bogus profile and assert a failure), + // its because when cdk-assets discovers the current account, it DOES consider the profile. + // writing a bogus profile would fail this operation and we won't be able to reach the code + // we're trying to test. + const credentialsFile = path.join(fixture.integTestDir, 'aws.credentials'); + const profile = 'cdk-assets'; + + // this kind sucks but its what it is given we need to write a working profile + await fs.writeFile(credentialsFile, `[${profile}] +aws_access_key_id=${currentCreds.accessKeyId} +aws_secret_access_key=${currentCreds.secretAccessKey} +aws_session_token=${currentCreds.sessionToken}`); + + await fs.writeFile(path.join(fixture.integTestDir, 'assets.json'), JSON.stringify(assetsJson, undefined, 2)); + await fixture.shell(['npx', 'cdk-assets', '--path', 'assets.json', 'publish', '--profile', profile], { + modEnv: { + ...fixture.cdkShellEnv(), + AWS_SHARED_CREDENTIALS_FILE: credentialsFile, + + // remove the default creds so that if the command doesn't use + // the profile, it will fail with "Could not load credentials from any providers" + AWS_ACCESS_KEY_ID: '', + AWS_SECRET_ACCESS_KEY: '', + AWS_SESSION_TOKEN: '', + + }, + }); +}), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-assets/smoketest.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-assets/smoketest.integtest.ts new file mode 100644 index 000000000..d2009acc0 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-assets/smoketest.integtest.ts @@ -0,0 +1,82 @@ +/** + * Tests for the standalone cdk-assets executable, as used by CDK Pipelines + */ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk-assets smoke test', + withDefaultFixture(async (fixture) => { + await fixture.shell(['npm', 'init', '-y']); + await fixture.shell(['npm', 'install', 'cdk-assets@latest']); + + const account = await fixture.aws.account(); + const region = fixture.aws.region; + const bucketName = `cdk-hnb659fds-assets-${account}-${region}`; + const repositoryName = `cdk-hnb659fds-container-assets-${account}-${region}`; + + const imageDir = 'imagedir'; + await fs.mkdir(path.join(fixture.integTestDir, imageDir), { recursive: true }); + + // Write an asset file and a data file for the Docker image + const assetFile = 'testfile.txt'; + for (const toCreate of [assetFile, `${imageDir}/datafile.txt`]) { + await fs.writeFile(path.join(fixture.integTestDir, toCreate), 'some asset file'); + } + + // Write a Dockerfile for the image build with a data file in it + await fs.writeFile(path.join(fixture.integTestDir, imageDir, 'Dockerfile'), [ + 'FROM scratch', + 'ADD datafile.txt datafile.txt', + ].join('\n')); + + // Write an asset JSON file to publish to the bootstrapped environment + const assetsJson = { + version: '38.0.1', + files: { + testfile: { + source: { + path: assetFile, + packaging: 'file', + }, + destinations: { + current: { + region, + assumeRoleArn: `arn:\${AWS::Partition}:iam::${account}:role/cdk-hnb659fds-file-publishing-role-${account}-${region}`, + bucketName, + objectKey: `test-file-${Date.now()}.json`, + }, + }, + }, + }, + dockerImages: { + testimage: { + source: { + directory: imageDir, + }, + destinations: { + current: { + region, + assumeRoleArn: `arn:\${AWS::Partition}:iam::${account}:role/cdk-hnb659fds-image-publishing-role-${account}-${region}`, + repositoryName, + imageTag: 'test-image', // Not fresh on every run because we'll run out of tags too easily + }, + }, + }, + }, + }; + + await fs.writeFile(path.join(fixture.integTestDir, 'assets.json'), JSON.stringify(assetsJson, undefined, 2)); + await fixture.shell(['npx', 'cdk-assets', '--path', 'assets.json', '--verbose', 'publish'], { + modEnv: { + ...fixture.cdkShellEnv(), + // This is necessary for cdk-assets v2, if the credentials are supplied via + // config file (which they are on the CodeBuild canaries). + AWS_SDK_LOAD_CONFIG: '1', + }, + }); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-ordering-with-concurrency.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-ordering-with-concurrency.integtest.ts new file mode 100644 index 000000000..b1e73611b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-ordering-with-concurrency.integtest.ts @@ -0,0 +1,15 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'automatic ordering with concurrency', + withDefaultFixture(async (fixture) => { + // Deploy the consuming stack which will include the producing stack + await fixture.cdkDeploy('order-consuming', { options: ['--concurrency', '2'] }); + + // Destroy the providing stack which will include the consuming stack + await fixture.cdkDestroy('order-providing'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-ordering.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-ordering.integtest.ts new file mode 100644 index 000000000..065f9cd0e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-ordering.integtest.ts @@ -0,0 +1,15 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'automatic ordering', + withDefaultFixture(async (fixture) => { + // Deploy the consuming stack which will include the producing stack + await fixture.cdkDeploy('order-consuming'); + + // Destroy the providing stack which will include the consuming stack + await fixture.cdkDestroy('order-providing'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-rollback-if-paused-and---no-rollback-is-removed-from-flags.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-rollback-if-paused-and---no-rollback-is-removed-from-flags.integtest.ts new file mode 100644 index 000000000..8b7acc2a7 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-rollback-if-paused-and---no-rollback-is-removed-from-flags.integtest.ts @@ -0,0 +1,40 @@ +import { integTest, withSpecificFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'automatic rollback if paused and --no-rollback is removed from flags', + withSpecificFixture('rollback-test-app', async (fixture) => { + let phase = '1'; + + // Should succeed + await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + }); + try { + phase = '2a'; + + // Should fail + const deployOutput = await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + allowErrExit: true, + }); + expect(deployOutput).toContain('UPDATE_FAILED'); + + // Do a deployment removing --no-rollback: this will roll back first and then deploy normally + phase = '1'; + await fixture.cdkDeploy('test-rollback', { + options: ['--force'], + modEnv: { PHASE: phase }, + verbose: false, + }); + } finally { + await fixture.cdkDestroy('test-rollback'); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-rollback-if-paused-and-change-contains-a-replacement.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-rollback-if-paused-and-change-contains-a-replacement.integtest.ts new file mode 100644 index 000000000..12b839132 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-rollback-if-paused-and-change-contains-a-replacement.integtest.ts @@ -0,0 +1,40 @@ +import { integTest, withSpecificFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'automatic rollback if paused and change contains a replacement', + withSpecificFixture('rollback-test-app', async (fixture) => { + let phase = '1'; + + // Should succeed + await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + }); + try { + phase = '2a'; + + // Should fail + const deployOutput = await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + allowErrExit: true, + }); + expect(deployOutput).toContain('UPDATE_FAILED'); + + // Do a deployment with a replacement and --force: this will roll back first and then deploy normally + phase = '3'; + await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback', '--force'], + modEnv: { PHASE: phase }, + verbose: false, + }); + } finally { + await fixture.cdkDestroy('test-rollback'); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-rollback-if-replacement-and---no-rollback-is-removed-from-flags.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-rollback-if-replacement-and---no-rollback-is-removed-from-flags.integtest.ts new file mode 100644 index 000000000..4e546e475 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-automatic-rollback-if-replacement-and---no-rollback-is-removed-from-flags.integtest.ts @@ -0,0 +1,29 @@ +import { integTest, withSpecificFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'automatic rollback if replacement and --no-rollback is removed from flags', + withSpecificFixture('rollback-test-app', async (fixture) => { + let phase = '1'; + + // Should succeed + await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + }); + try { + // Do a deployment with a replacement and removing --no-rollback: this will do a regular rollback deploy + phase = '3'; + await fixture.cdkDeploy('test-rollback', { + options: ['--force'], + modEnv: { PHASE: phase }, + verbose: false, + }); + } finally { + await fixture.cdkDestroy('test-rollback'); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-a-customized-template-vendor-will-not-overwrite-the-default-template.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-a-customized-template-vendor-will-not-overwrite-the-default-template.integtest.ts new file mode 100644 index 000000000..408f55d09 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-a-customized-template-vendor-will-not-overwrite-the-default-template.integtest.ts @@ -0,0 +1,42 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as yaml from 'yaml'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('a customized template vendor will not overwrite the default template', withoutBootstrap(async (fixture) => { + // Initial bootstrap + const toolkitStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapModern({ + toolkitStackName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + + // Customize template + const templateStr = await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName, + showTemplate: true, + cliOptions: { + captureStderr: false, + }, + }); + + const template = yaml.parse(templateStr, { schema: 'core' }); + template.Parameters.BootstrapVariant.Default = 'CustomizedVendor'; + const filename = path.join(fixture.integTestDir, `${fixture.qualifier}-template.yaml`); + fs.writeFileSync(filename, yaml.stringify(template, { schema: 'yaml-1.1' }), { encoding: 'utf-8' }); + + // Rebootstrap. For some reason, this doesn't cause a failure, it's a successful no-op. + const output = await fixture.cdkBootstrapModern({ + toolkitStackName, + template: filename, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + cliOptions: { + captureStderr: true, + }, + }); + expect(output).toContain('Not overwriting it with a template containing'); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-add-tags.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-add-tags.integtest.ts new file mode 100644 index 000000000..c15508fb5 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-add-tags.integtest.ts @@ -0,0 +1,26 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('add tags, left alone on re-bootstrap', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + tags: 'Foo=Bar', + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + force: true, + }); + + const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: bootstrapStackName })); + expect(response.Stacks?.[0].Tags).toEqual([ + { Key: 'Foo', Value: 'Bar' }, + ]); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-add-tags-then-update-tags-during-re-bootstrap.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-add-tags-then-update-tags-during-re-bootstrap.integtest.ts new file mode 100644 index 000000000..a5f944f8c --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-add-tags-then-update-tags-during-re-bootstrap.integtest.ts @@ -0,0 +1,28 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can add tags then update tags during re-bootstrap', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + tags: 'Foo=Bar', + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + tags: 'Foo=BarBaz', + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + force: true, + }); + + const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: bootstrapStackName })); + expect(response.Stacks?.[0].Tags).toEqual([ + { Key: 'Foo', Value: 'BarBaz' }, + ]); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-and-deploy-if-omitting-execution-policies.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-and-deploy-if-omitting-execution-policies.integtest.ts new file mode 100644 index 000000000..ec6ef0837 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-and-deploy-if-omitting-execution-policies.integtest.ts @@ -0,0 +1,21 @@ +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can and deploy if omitting execution policies', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + }); + + // Deploy stack that uses file assets + await fixture.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-bootstrap-without-execution.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-bootstrap-without-execution.integtest.ts new file mode 100644 index 000000000..7cb20c941 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-bootstrap-without-execution.integtest.ts @@ -0,0 +1,22 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can bootstrap without execution', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapLegacy({ + toolkitStackName: bootstrapStackName, + noExecute: true, + }); + + const resp = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: bootstrapStackName, + }), + ); + + expect(resp.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-create-a-legacy-bootstrap-stack-with---public-access-block-configuration-false.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-create-a-legacy-bootstrap-stack-with---public-access-block-configuration-false.integtest.ts new file mode 100644 index 000000000..e115f3825 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-create-a-legacy-bootstrap-stack-with---public-access-block-configuration-false.integtest.ts @@ -0,0 +1,21 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can create a legacy bootstrap stack with --public-access-block-configuration=false', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapLegacy({ + verbose: true, + toolkitStackName: bootstrapStackName, + publicAccessBlockConfiguration: false, + tags: 'Foo=Bar', + }); + + const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: bootstrapStackName })); + expect(response.Stacks?.[0].Tags).toEqual([ + { Key: 'Foo', Value: 'Bar' }, + ]); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-create-multiple-legacy-bootstrap-stacks.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-create-multiple-legacy-bootstrap-stacks.integtest.ts new file mode 100644 index 000000000..ecafa88eb --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-create-multiple-legacy-bootstrap-stacks.integtest.ts @@ -0,0 +1,27 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can create multiple legacy bootstrap stacks', withoutBootstrap(async (fixture) => { + const bootstrapStackName1 = `${fixture.bootstrapStackName}-1`; + const bootstrapStackName2 = `${fixture.bootstrapStackName}-2`; + + // deploy two toolkit stacks into the same environment (see #1416) + // one with tags + await fixture.cdkBootstrapLegacy({ + verbose: true, + toolkitStackName: bootstrapStackName1, + tags: 'Foo=Bar', + }); + await fixture.cdkBootstrapLegacy({ + verbose: true, + toolkitStackName: bootstrapStackName2, + }); + + const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: bootstrapStackName1 })); + expect(response.Stacks?.[0].Tags).toEqual([ + { Key: 'Foo', Value: 'Bar' }, + ]); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-deploy-modern-synthesized-stack-even-if-bootstrap-stack-name-is-unknown.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-deploy-modern-synthesized-stack-even-if-bootstrap-stack-name-is-unknown.integtest.ts new file mode 100644 index 000000000..777f6c801 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-deploy-modern-synthesized-stack-even-if-bootstrap-stack-name-is-unknown.integtest.ts @@ -0,0 +1,24 @@ +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can deploy modern-synthesized stack even if bootstrap stack name is unknown', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + + // Deploy stack that uses file assets + await fixture.cdkDeploy('lambda', { + options: [ + // Explicity pass a name that's sure to not exist, otherwise the CLI might accidentally find a + // default bootstracp stack if that happens to be in the account already. + '--toolkit-stack-name', 'DefinitelyDoesNotExist', + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-deploy-with-session-tags-on-the-deploy.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-deploy-with-session-tags-on-the-deploy.integtest.ts new file mode 100644 index 000000000..26527fe10 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-deploy-with-session-tags-on-the-deploy.integtest.ts @@ -0,0 +1,25 @@ +import * as path from 'path'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can deploy with session tags on the deploy, lookup, file asset, and image asset publishing roles', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + bootstrapTemplate: path.join(__dirname, '..', '..', 'resources', 'bootstrap-templates', 'session-tags.all-roles-deny-all.yaml'), + }); + + await fixture.cdkDeploy('session-tags', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + modEnv: { + ENABLE_VPC_TESTING: 'IMPORT', + }, + }); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-deploy-without-execution-role-and-with-session-tags-on-deploy-role.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-deploy-without-execution-role-and-with-session-tags-on-deploy-role.integtest.ts new file mode 100644 index 000000000..f94f7b59c --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-deploy-without-execution-role-and-with-session-tags-on-deploy-role.integtest.ts @@ -0,0 +1,22 @@ +import * as path from 'path'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can deploy without execution role and with session tags on deploy role', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + bootstrapTemplate: path.join(__dirname, '..', '..', 'resources', 'bootstrap-templates', 'session-tags.deploy-role-deny-sqs.yaml'), + }); + + await fixture.cdkDeploy('session-tags-with-custom-synthesizer', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-dump-the-template.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-dump-the-template.integtest.ts new file mode 100644 index 000000000..5ed36b1be --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-dump-the-template.integtest.ts @@ -0,0 +1,32 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can dump the template, modify and use it to deploy a custom bootstrap stack', withoutBootstrap(async (fixture) => { + let template = await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: fixture.bootstrapStackName, + showTemplate: true, + cliOptions: { + captureStderr: false, + }, + }); + + expect(template).toContain('BootstrapVersion:'); + + template += '\n' + [ + ' TwiddleDee:', + ' Value: Template got twiddled', + ].join('\n'); + + const filename = path.join(fixture.integTestDir, `${fixture.qualifier}-template.yaml`); + fs.writeFileSync(filename, template, { encoding: 'utf-8' }); + await fixture.cdkBootstrapModern({ + toolkitStackName: fixture.bootstrapStackName, + template: filename, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-remove-custompermissionsboundary.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-remove-custompermissionsboundary.integtest.ts new file mode 100644 index 000000000..d2c41efeb --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-remove-custompermissionsboundary.integtest.ts @@ -0,0 +1,77 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { CreatePolicyCommand, DeletePolicyCommand, GetRoleCommand } from '@aws-sdk/client-iam'; +import { integTest, withoutBootstrap } from '../../lib'; +import eventually from '../../lib/eventually'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can remove customPermissionsBoundary', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + const policyName = `${bootstrapStackName}-pb`; + let policyArn; + try { + const policy = await fixture.aws.iam.send( + new CreatePolicyCommand({ + PolicyName: policyName, + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: { + Action: ['*'], + Resource: ['*'], + Effect: 'Allow', + }, + }), + }), + ); + policyArn = policy.Policy?.Arn; + + // Policy creation and consistency across regions is "almost immediate" + // See: https://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_general.html#troubleshoot_general_eventual-consistency + // We will put this in an `eventually` block to retry stack creation with a reasonable timeout + const createStackWithPermissionBoundary = async (): Promise => { + await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: bootstrapStackName, + customPermissionsBoundary: policyName, + }); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ StackName: bootstrapStackName }), + ); + expect( + response.Stacks?.[0].Parameters?.some( + param => (param.ParameterKey === 'InputPermissionsBoundary' && param.ParameterValue === policyName), + )).toEqual(true); + }; + + await eventually(createStackWithPermissionBoundary, { maxAttempts: 3 }); + + await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: bootstrapStackName, + usePreviousParameters: false, + }); + const response2 = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ StackName: bootstrapStackName }), + ); + expect( + response2.Stacks?.[0].Parameters?.some( + param => (param.ParameterKey === 'InputPermissionsBoundary' && !param.ParameterValue), + )).toEqual(true); + + const region = fixture.aws.region; + const account = await fixture.aws.account(); + const role = await fixture.aws.iam.send( + new GetRoleCommand({ RoleName: `cdk-${fixture.qualifier}-cfn-exec-role-${account}-${region}` }), + ); + if (!role.Role) { + throw new Error('Role not found'); + } + expect(role.Role.PermissionsBoundary).toBeUndefined(); + } finally { + if (policyArn) { + await fixture.aws.iam.send(new DeletePolicyCommand({ PolicyArn: policyArn })); + } + } +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-remove-trusted-account.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-remove-trusted-account.integtest.ts new file mode 100644 index 000000000..b9c7dc25a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-remove-trusted-account.integtest.ts @@ -0,0 +1,30 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can remove trusted account', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + verbose: false, + toolkitStackName: bootstrapStackName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + trust: ['599757620138', '730170552321'], + }); + + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + cfnExecutionPolicy: ' arn:aws:iam::aws:policy/AdministratorAccess', + untrust: ['730170552321'], + }); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ StackName: bootstrapStackName }), + ); + + const trustedAccounts = response.Stacks?.[0].Parameters?.find(p => p.ParameterKey === 'TrustedAccounts')?.ParameterValue; + expect(trustedAccounts).toEqual('599757620138'); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-use-the-custom-permissions-boundary-(with-slashes)-to-bootstrap.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-use-the-custom-permissions-boundary-(with-slashes)-to-bootstrap.integtest.ts new file mode 100644 index 000000000..be636269e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-use-the-custom-permissions-boundary-(with-slashes)-to-bootstrap.integtest.ts @@ -0,0 +1,15 @@ +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can use the custom permissions boundary (with slashes) to bootstrap', withoutBootstrap(async (fixture) => { + let template = await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: fixture.bootstrapStackName, + showTemplate: true, + customPermissionsBoundary: 'permission-boundary-name/with/path', + }); + + expect(template).toContain('permission-boundary-name/with/path'); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-use-the-custom-permissions-boundary-to-bootstrap.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-use-the-custom-permissions-boundary-to-bootstrap.integtest.ts new file mode 100644 index 000000000..17f88bd37 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-use-the-custom-permissions-boundary-to-bootstrap.integtest.ts @@ -0,0 +1,15 @@ +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can use the custom permissions boundary to bootstrap', withoutBootstrap(async (fixture) => { + let template = await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: fixture.bootstrapStackName, + showTemplate: true, + customPermissionsBoundary: 'permission-boundary-name', + }); + + expect(template).toContain('permission-boundary-name'); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-use-the-default-permissions-boundary-to-bootstrap.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-use-the-default-permissions-boundary-to-bootstrap.integtest.ts new file mode 100644 index 000000000..6a1eff070 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-can-use-the-default-permissions-boundary-to-bootstrap.integtest.ts @@ -0,0 +1,15 @@ +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('can use the default permissions boundary to bootstrap', withoutBootstrap(async (fixture) => { + let template = await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: fixture.bootstrapStackName, + showTemplate: true, + examplePermissionsBoundary: true, + }); + + expect(template).toContain('PermissionsBoundary'); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-create-ecr-with-tag-immutability-to-set-on.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-create-ecr-with-tag-immutability-to-set-on.integtest.ts new file mode 100644 index 000000000..0036f0825 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-create-ecr-with-tag-immutability-to-set-on.integtest.ts @@ -0,0 +1,34 @@ +import { DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation'; +import { DescribeRepositoriesCommand } from '@aws-sdk/client-ecr'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('create ECR with tag IMMUTABILITY to set on', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + }); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStackResourcesCommand({ + StackName: bootstrapStackName, + }), + ); + const ecrResource = response.StackResources?.find(resource => resource.LogicalResourceId === 'ContainerAssetsRepository'); + expect(ecrResource).toBeDefined(); + + const ecrResponse = await fixture.aws.ecr.send( + new DescribeRepositoriesCommand({ + repositoryNames: [ + // This is set, as otherwise we don't end up here + ecrResource?.PhysicalResourceId ?? '', + ], + }), + ); + + expect(ecrResponse.repositories?.[0].imageTagMutability).toEqual('IMMUTABLE'); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-deploy-new-style-synthesis-to-new-style-bootstrap-(with-docker-image).integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-deploy-new-style-synthesis-to-new-style-bootstrap-(with-docker-image).integtest.ts new file mode 100644 index 000000000..036879e23 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-deploy-new-style-synthesis-to-new-style-bootstrap-(with-docker-image).integtest.ts @@ -0,0 +1,22 @@ +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('deploy new style synthesis to new style bootstrap (with docker image)', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + + // Deploy stack that uses file assets + await fixture.cdkDeploy('docker', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-deploy-new-style-synthesis-to-new-style-bootstrap.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-deploy-new-style-synthesis-to-new-style-bootstrap.integtest.ts new file mode 100644 index 000000000..2b8c137f6 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-deploy-new-style-synthesis-to-new-style-bootstrap.integtest.ts @@ -0,0 +1,22 @@ +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('deploy new style synthesis to new style bootstrap', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + + // Deploy stack that uses file assets + await fixture.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--context', '@aws-cdk/core:newStyleStackSynthesis=1', + ], + }); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-deploy-old-style-synthesis-to-new-style-bootstrap.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-deploy-old-style-synthesis-to-new-style-bootstrap.integtest.ts new file mode 100644 index 000000000..e961f9c53 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-deploy-old-style-synthesis-to-new-style-bootstrap.integtest.ts @@ -0,0 +1,21 @@ +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('deploy old style synthesis to new style bootstrap', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + + // Deploy stack that uses file assets + await fixture.cdkDeploy('lambda', { + options: [ + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', bootstrapStackName, + ], + }); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-switch-on-termination-protection.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-switch-on-termination-protection.integtest.ts new file mode 100644 index 000000000..f148b9059 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-switch-on-termination-protection.integtest.ts @@ -0,0 +1,24 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('switch on termination protection, switch is left alone on re-bootstrap', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + terminationProtection: true, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: bootstrapStackName, + force: true, + }); + + const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: bootstrapStackName })); + expect(response.Stacks?.[0].EnableTerminationProtection).toEqual(true); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-upgrade-legacy-bootstrap-stack-to-new-bootstrap-stack-while-in-use.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-upgrade-legacy-bootstrap-stack-to-new-bootstrap-stack-while-in-use.integtest.ts new file mode 100644 index 000000000..a5a482d32 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-bootstrap-upgrade-legacy-bootstrap-stack-to-new-bootstrap-stack-while-in-use.integtest.ts @@ -0,0 +1,47 @@ +import { integTest, withoutBootstrap, randomString } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('upgrade legacy bootstrap stack to new bootstrap stack while in use', withoutBootstrap(async (fixture) => { + const bootstrapStackName = fixture.bootstrapStackName; + + const legacyBootstrapBucketName = `aws-cdk-bootstrap-integ-test-legacy-bckt-${randomString()}`; + const newBootstrapBucketName = `aws-cdk-bootstrap-integ-test-v2-bckt-${randomString()}`; + fixture.rememberToDeleteBucket(legacyBootstrapBucketName); // This one will leak + fixture.rememberToDeleteBucket(newBootstrapBucketName); // This one shouldn't leak if the test succeeds, but let's be safe in case it doesn't + + // Legacy bootstrap + await fixture.cdkBootstrapLegacy({ + toolkitStackName: bootstrapStackName, + bootstrapBucketName: legacyBootstrapBucketName, + }); + + // Deploy stack that uses file assets + await fixture.cdkDeploy('lambda', { + options: [ + '--context', `bootstrapBucket=${legacyBootstrapBucketName}`, + '--context', 'legacySynth=true', + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', bootstrapStackName, + ], + }); + + // Upgrade bootstrap stack to "new" style + await fixture.cdkBootstrapModern({ + toolkitStackName: bootstrapStackName, + bootstrapBucketName: newBootstrapBucketName, + cfnExecutionPolicy: 'arn:aws:iam::aws:policy/AdministratorAccess', + }); + + // (Force) deploy stack again + // --force to bypass the check which says that the template hasn't changed. + await fixture.cdkDeploy('lambda', { + options: [ + '--context', `bootstrapBucket=${newBootstrapBucketName}`, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', bootstrapStackName, + '--force', + ], + }); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-can-still-load-old-assemblies.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-can-still-load-old-assemblies.integtest.ts new file mode 100644 index 000000000..24bd0553d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-can-still-load-old-assemblies.integtest.ts @@ -0,0 +1,57 @@ +import { promises as fs } from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { integTest, RESOURCES_DIR, shell, withDefaultFixture, cloneDirectory } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'can still load old assemblies', + withDefaultFixture(async (fixture) => { + const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx'); + + const testAssembliesDirectory = path.join(RESOURCES_DIR, 'cloud-assemblies'); + for (const asmdir of await listChildDirs(testAssembliesDirectory)) { + fixture.log(`ASSEMBLY ${asmdir}`); + await cloneDirectory(asmdir, cxAsmDir); + + // Some files in the asm directory that have a .js extension are + // actually treated as templates. Evaluate them using NodeJS. + const templates = await listChildren(cxAsmDir, (fullPath) => Promise.resolve(fullPath.endsWith('.js'))); + for (const template of templates) { + const targetName = template.replace(/.js$/, ''); + await shell([process.execPath, template, '>', targetName], { + cwd: cxAsmDir, + outputs: [fixture.output], + modEnv: { + TEST_ACCOUNT: await fixture.aws.account(), + TEST_REGION: fixture.aws.region, + }, + }); + } + + // Use this directory as a Cloud Assembly + const output = await fixture.cdk(['--app', cxAsmDir, '-v', 'synth']); + + // Assert that there was no providerError in CDK's stderr + // Because we rely on the app/framework to actually error in case the + // provider fails, we inspect the logs here. + expect(output).not.toContain('$providerError'); + } + }), +); + +async function listChildren(parent: string, pred: (x: string) => Promise) { + const ret = new Array(); + for (const child of await fs.readdir(parent, { encoding: 'utf-8' })) { + const fullPath = path.join(parent, child.toString()); + if (await pred(fullPath)) { + ret.push(fullPath); + } + } + return ret; +} + +async function listChildDirs(parent: string) { + return listChildren(parent, async (fullPath: string) => (await fs.stat(fullPath)).isDirectory()); +} diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---fail-on-multiple-stacks-exits-with-error-if-any-of-the-stacks-contains-a-diff.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---fail-on-multiple-stacks-exits-with-error-if-any-of-the-stacks-contains-a-diff.integtest.ts new file mode 100644 index 000000000..f32267db2 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---fail-on-multiple-stacks-exits-with-error-if-any-of-the-stacks-contains-a-diff.integtest.ts @@ -0,0 +1,22 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --fail on multiple stacks exits with error if any of the stacks contains a diff', + withDefaultFixture(async (fixture) => { + // GIVEN + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + + await fixture.cdkDeploy('test-2'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('There were no differences'); + + // WHEN / THEN + await expect( + fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')]), + ).rejects.toThrow('exited with error'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---fail-with-multiple-stack-exits-with-if-any-of-the-stacks-contains-a-diff.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---fail-with-multiple-stack-exits-with-if-any-of-the-stacks-contains-a-diff.integtest.ts new file mode 100644 index 000000000..c439e9761 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---fail-with-multiple-stack-exits-with-if-any-of-the-stacks-contains-a-diff.integtest.ts @@ -0,0 +1,22 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --fail with multiple stack exits with if any of the stacks contains a diff', + withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('test-1'); + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('There were no differences'); + + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + + // WHEN / THEN + await expect( + fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')]), + ).rejects.toThrow('exited with error'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---quiet-does-not-print-there-were-no-differences-message-for-stacks-which-have-no-differences.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---quiet-does-not-print-there-were-no-differences-message-for-stacks-which-have-no-differences.integtest.ts new file mode 100644 index 000000000..485397586 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---quiet-does-not-print-there-were-no-differences-message-for-stacks-which-have-no-differences.integtest.ts @@ -0,0 +1,19 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + "cdk diff --quiet does not print 'There were no differences' message for stacks which have no differences", + withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('test-1'); + + // WHEN + const diff = await fixture.cdk(['diff', '--quiet', fixture.fullStackName('test-1')]); + + // THEN + expect(diff).not.toContain('Stack test-1'); + expect(diff).not.toContain('There were no differences'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-changes-are-present.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-changes-are-present.integtest.ts new file mode 100644 index 000000000..9d044ed8d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-changes-are-present.integtest.ts @@ -0,0 +1,14 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --security-only --fail exits when security changes are present', + withDefaultFixture(async (fixture) => { + const stackName = 'iam-test'; + await expect(fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName(stackName)])).rejects.toThrow( + 'exited with error', + ); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-access-control-config.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-access-control-config.integtest.ts new file mode 100644 index 000000000..795de12f0 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-access-control-config.integtest.ts @@ -0,0 +1,13 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --security-only --fail exits when security diff for sso access control config', + withDefaultFixture(async (fixture) => { + await expect( + fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-access-control')]), + ).rejects.toThrow('exited with error'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-assignment.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-assignment.integtest.ts new file mode 100644 index 000000000..3ae31b42f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-assignment.integtest.ts @@ -0,0 +1,13 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --security-only --fail exits when security diff for sso-assignment', + withDefaultFixture(async (fixture) => { + await expect( + fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-assignment')]), + ).rejects.toThrow('exited with error'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-perm-set-with-managed-policy.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-perm-set-with-managed-policy.integtest.ts new file mode 100644 index 000000000..c103f3bfe --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-perm-set-with-managed-policy.integtest.ts @@ -0,0 +1,13 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --security-only --fail exits when security diff for sso-perm-set-with-managed-policy', + withDefaultFixture(async (fixture) => { + await expect( + fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-perm-set-with-managed-policy')]), + ).rejects.toThrow('exited with error'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-perm-set-without-managed-policy.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-perm-set-without-managed-policy.integtest.ts new file mode 100644 index 000000000..b3c13911f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only---fail-exits-when-security-diff-for-sso-perm-set-without-managed-policy.integtest.ts @@ -0,0 +1,13 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --security-only --fail exits when security diff for sso-perm-set-without-managed-policy', + withDefaultFixture(async (fixture) => { + await expect( + fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName('sso-perm-set-without-managed-policy')]), + ).rejects.toThrow('exited with error'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-access-control-information.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-access-control-information.integtest.ts new file mode 100644 index 000000000..2621e2bde --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-access-control-information.integtest.ts @@ -0,0 +1,35 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --security-only successfully outputs sso-access-control information', + withDefaultFixture(async (fixture) => { + const diff = await fixture.cdk(['diff', '--security-only', fixture.fullStackName('sso-access-control')]); + `┌───┬────────────────────────────────┬────────────────────────┬─────────────────────────────────┐ + │ │ Resource │ InstanceArn │ AccessControlAttributes │ + ├───┼────────────────────────────────┼────────────────────────┼─────────────────────────────────┤ + │ + │\${instanceAccessControlConfig} │ arn:aws:test:testvalue │ Key: first, Values: [a] │ + │ │ │ │ Key: second, Values: [b] │ + │ │ │ │ Key: third, Values: [c] │ + │ │ │ │ Key: fourth, Values: [d] │ + │ │ │ │ Key: fifth, Values: [e] │ + │ │ │ │ Key: sixth, Values: [f] │ + └───┴────────────────────────────────┴────────────────────────┴─────────────────────────────────┘ +`; + expect(diff).toContain('Resource'); + expect(diff).toContain('instanceAccessControlConfig'); + + expect(diff).toContain('InstanceArn'); + expect(diff).toContain('arn:aws:sso:::instance/testvalue'); + + expect(diff).toContain('AccessControlAttributes'); + expect(diff).toContain('Key: first, Values: [a]'); + expect(diff).toContain('Key: second, Values: [b]'); + expect(diff).toContain('Key: third, Values: [c]'); + expect(diff).toContain('Key: fourth, Values: [d]'); + expect(diff).toContain('Key: fifth, Values: [e]'); + expect(diff).toContain('Key: sixth, Values: [f]'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-assignment-information.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-assignment-information.integtest.ts new file mode 100644 index 000000000..247ed171a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-assignment-information.integtest.ts @@ -0,0 +1,37 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --security-only successfully outputs sso-assignment information', + withDefaultFixture(async (fixture) => { + const diff = await fixture.cdk(['diff', '--security-only', fixture.fullStackName('sso-assignment')]); + `┌───┬───────────────┬──────────────────────────────────┬─────────────────────────┬──────────────────────────────┬───────────────┬──────────────┬─────────────┐ + │ │ Resource │ InstanceArn │ PermissionSetArn │ PrincipalId │ PrincipalType │ TargetId │ TargetType │ + ├───┼───────────────┼──────────────────────────────────┼─────────────────────────┼──────────────────────────────┼───────────────┼──────────────┼─────────────┤ + │ + │\${assignment} │ arn:aws:sso:::instance/testvalue │ arn:aws:sso:::testvalue │ 11111111-2222-3333-4444-test │ USER │ 111111111111 │ AWS_ACCOUNT │ + └───┴───────────────┴──────────────────────────────────┴─────────────────────────┴──────────────────────────────┴───────────────┴──────────────┴─────────────┘ +`; + expect(diff).toContain('Resource'); + expect(diff).toContain('assignment'); + + expect(diff).toContain('InstanceArn'); + expect(diff).toContain('arn:aws:sso:::instance/testvalue'); + + expect(diff).toContain('PermissionSetArn'); + expect(diff).toContain('arn:aws:sso:::testvalue'); + + expect(diff).toContain('PrincipalId'); + expect(diff).toContain('11111111-2222-3333-4444-test'); + + expect(diff).toContain('PrincipalType'); + expect(diff).toContain('USER'); + + expect(diff).toContain('TargetId'); + expect(diff).toContain('111111111111'); + + expect(diff).toContain('TargetType'); + expect(diff).toContain('AWS_ACCOUNT'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-permission-set-with-managed-policy-information.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-permission-set-with-managed-policy-information.integtest.ts new file mode 100644 index 000000000..3b070a565 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-permission-set-with-managed-policy-information.integtest.ts @@ -0,0 +1,35 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --security-only successfully outputs sso-permission-set-with-managed-policy information', + withDefaultFixture(async (fixture) => { + const diff = await fixture.cdk([ + 'diff', + '--security-only', + fixture.fullStackName('sso-perm-set-with-managed-policy'), + ]); + `┌───┬──────────────────────────────────────────┬──────────────────────────────────┬────────────────────┬───────────────────────────────────────────────────────────────┬─────────────────────────────────┐ + │ │ Resource │ InstanceArn │ PermissionSet name │ PermissionsBoundary │ CustomerManagedPolicyReferences │ + ├───┼──────────────────────────────────────────┼──────────────────────────────────┼────────────────────┼───────────────────────────────────────────────────────────────┼─────────────────────────────────┤ + │ + │\${permission-set-with-managed-policy} │ arn:aws:sso:::instance/testvalue │ niceWork │ ManagedPolicyArn: arn:aws:iam::aws:policy/AdministratorAccess │ Name: forSSO, Path: │ +`; + + expect(diff).toContain('Resource'); + expect(diff).toContain('permission-set-with-managed-policy'); + + expect(diff).toContain('InstanceArn'); + expect(diff).toContain('arn:aws:sso:::instance/testvalue'); + + expect(diff).toContain('PermissionSet name'); + expect(diff).toContain('niceWork'); + + expect(diff).toContain('PermissionsBoundary'); + expect(diff).toContain('ManagedPolicyArn: arn:aws:iam::aws:policy/AdministratorAccess'); + + expect(diff).toContain('CustomerManagedPolicyReferences'); + expect(diff).toContain('Name: forSSO, Path:'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-permission-set-without-managed-policy-information.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-permission-set-without-managed-policy-information.integtest.ts new file mode 100644 index 000000000..53485e28f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff---security-only-successfully-outputs-sso-permission-set-without-managed-policy-information.integtest.ts @@ -0,0 +1,37 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --security-only successfully outputs sso-permission-set-without-managed-policy information', + withDefaultFixture(async (fixture) => { + const diff = await fixture.cdk([ + 'diff', + '--security-only', + fixture.fullStackName('sso-perm-set-without-managed-policy'), + ]); + `┌───┬──────────────────────────────────────────┬──────────────────────────────────┬────────────────────┬───────────────────────────────────┬─────────────────────────────────┐ + │ │ Resource │ InstanceArn │ PermissionSet name │ PermissionsBoundary │ CustomerManagedPolicyReferences │ + ├───┼──────────────────────────────────────────┼──────────────────────────────────┼────────────────────┼───────────────────────────────────┼─────────────────────────────────┤ + │ + │\${permission-set-without-managed-policy} │ arn:aws:sso:::instance/testvalue │ testName │ CustomerManagedPolicyReference: { │ │ + │ │ │ │ │ Name: why, Path: /how/ │ │ + │ │ │ │ │ } │ │ +`; + expect(diff).toContain('Resource'); + expect(diff).toContain('permission-set-without-managed-policy'); + + expect(diff).toContain('InstanceArn'); + expect(diff).toContain('arn:aws:sso:::instance/testvalue'); + + expect(diff).toContain('PermissionSet name'); + expect(diff).toContain('testName'); + + expect(diff).toContain('PermissionsBoundary'); + expect(diff).toContain('CustomerManagedPolicyReference: {'); + expect(diff).toContain('Name: why, Path: /how/'); + expect(diff).toContain('}'); + + expect(diff).toContain('CustomerManagedPolicyReferences'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-doesnt-show-resource-metadata-changes.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-doesnt-show-resource-metadata-changes.integtest.ts new file mode 100644 index 000000000..bb1d980fc --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-doesnt-show-resource-metadata-changes.integtest.ts @@ -0,0 +1,23 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff doesnt show resource metadata changes', + withDefaultFixture(async (fixture) => { + // GIVEN - small initial stack with default resource metadata + await fixture.cdkDeploy('metadata'); + + // WHEN - changing resource metadata value + const diff = await fixture.cdk(['diff', fixture.fullStackName('metadata')], { + verbose: true, + modEnv: { + INTEG_METADATA_VALUE: 'custom', + }, + }); + + // Assert there are no changes + expect(diff).toContain('There were no differences'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-shows-resource-metadata-changes-with---no-change-set.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-shows-resource-metadata-changes-with---no-change-set.integtest.ts new file mode 100644 index 000000000..e7c82314b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-shows-resource-metadata-changes-with---no-change-set.integtest.ts @@ -0,0 +1,23 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff shows resource metadata changes with --no-change-set', + withDefaultFixture(async (fixture) => { + // GIVEN - small initial stack with default resource metadata + await fixture.cdkDeploy('metadata'); + + // WHEN - changing resource metadata value + const diff = await fixture.cdk(['diff --no-change-set', fixture.fullStackName('metadata')], { + verbose: true, + modEnv: { + INTEG_METADATA_VALUE: 'custom', + }, + }); + + // Assert there are changes + expect(diff).not.toContain('There were no differences'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-with-large-changeset-and-custom-toolkit-stack-name-and-qualifier-does-not-fail.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-with-large-changeset-and-custom-toolkit-stack-name-and-qualifier-does-not-fail.integtest.ts new file mode 100644 index 000000000..ff5c8c3ba --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-with-large-changeset-and-custom-toolkit-stack-name-and-qualifier-does-not-fail.integtest.ts @@ -0,0 +1,39 @@ +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('cdk diff with large changeset and custom toolkit stack name and qualifier does not fail', withoutBootstrap(async (fixture) => { + // Bootstrapping with custom toolkit stack name and qualifier + const qualifier = fixture.qualifier; + + const toolkitStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: toolkitStackName, + qualifier: qualifier, + }); + + // Deploying small initial stack with only one IAM role + await fixture.cdkDeploy('iam-roles', { + modEnv: { + NUMBER_OF_ROLES: '1', + }, + options: [ + '--toolkit-stack-name', toolkitStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${qualifier}`, + ], + }); + + // WHEN - adding a role with a ton of metadata to create a large diff + const diff = await fixture.cdk(['diff', '--toolkit-stack-name', toolkitStackName, '--context', `@aws-cdk/core:bootstrapQualifier=${qualifier}`, fixture.fullStackName('iam-roles')], { + verbose: true, + modEnv: { + NUMBER_OF_ROLES: '2', + }, + }); + + // Assert that the CLI assumes the file publishing role: + expect(diff).toMatch(/Assuming role .*file-publishing-role/); + expect(diff).toContain('success: Published'); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-with-large-changeset-does-not-fail.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-with-large-changeset-does-not-fail.integtest.ts new file mode 100644 index 000000000..5be396009 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff-with-large-changeset-does-not-fail.integtest.ts @@ -0,0 +1,28 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff with large changeset does not fail', + withDefaultFixture(async (fixture) => { + // GIVEN - small initial stack with only one IAM role + await fixture.cdkDeploy('iam-roles', { + modEnv: { + NUMBER_OF_ROLES: '1', + }, + }); + + // WHEN - adding an additional role with a ton of metadata to create a large diff + const diff = await fixture.cdk(['diff', fixture.fullStackName('iam-roles')], { + verbose: true, + modEnv: { + NUMBER_OF_ROLES: '2', + }, + }); + + // Assert that the CLI assumes the file publishing role: + expect(diff).toMatch(/Assuming role .*file-publishing-role/); + expect(diff).toContain('success: Published'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff.integtest.ts new file mode 100644 index 000000000..eb5bb6a71 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff.integtest.ts @@ -0,0 +1,18 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff', + withDefaultFixture(async (fixture) => { + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + + // We can make it fail by passing --fail + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1')])).rejects.toThrow('exited with error'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-ls---show-dependencies---json---long.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-ls---show-dependencies---json---long.integtest.ts new file mode 100644 index 000000000..2df460434 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-ls---show-dependencies---json---long.integtest.ts @@ -0,0 +1,50 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk ls --show-dependencies --json --long', + withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls --show-dependencies --json --long'], { captureStderr: false }); + + const expectedStacks = [ + { + id: 'order-providing', + name: 'order-providing', + enviroment: { + account: 'unknown-account', + region: 'unknown-region', + name: 'aws://unknown-account/unknown-region', + }, + dependencies: [], + }, + { + id: 'order-consuming', + name: 'order-consuming', + enviroment: { + account: 'unknown-account', + region: 'unknown-region', + name: 'aws://unknown-account/unknown-region', + }, + dependencies: [ + { + id: 'order-providing', + dependencies: [], + }, + ], + }, + ]; + + for (const stack of expectedStacks) { + expect(listing).toContain(fixture.fullStackName(stack.id)); + expect(listing).toContain(fixture.fullStackName(stack.name)); + expect(listing).toContain(stack.enviroment.account); + expect(listing).toContain(stack.enviroment.name); + expect(listing).toContain(stack.enviroment.region); + for (const dependency of stack.dependencies) { + expect(listing).toContain(fixture.fullStackName(dependency.id)); + } + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-ls---show-dependencies---json.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-ls---show-dependencies---json.integtest.ts new file mode 100644 index 000000000..9ceedb445 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-ls---show-dependencies---json.integtest.ts @@ -0,0 +1,95 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk ls --show-dependencies --json', + withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls --show-dependencies --json'], { captureStderr: false }); + + const expectedStacks = [ + { + id: 'test-1', + dependencies: [], + }, + { + id: 'order-providing', + dependencies: [], + }, + { + id: 'order-consuming', + dependencies: [ + { + id: 'order-providing', + dependencies: [], + }, + ], + }, + { + id: 'with-nested-stack', + dependencies: [], + }, + { + id: 'list-stacks', + dependencies: [ + { + id: 'list-stacks/DependentStack', + dependencies: [ + { + id: 'list-stacks/DependentStack/InnerDependentStack', + dependencies: [], + }, + ], + }, + ], + }, + { + id: 'list-multiple-dependent-stacks', + dependencies: [ + { + id: 'list-multiple-dependent-stacks/DependentStack1', + dependencies: [], + }, + { + id: 'list-multiple-dependent-stacks/DependentStack2', + dependencies: [], + }, + ], + }, + ]; + + function validateStackDependencies(stack: StackDetails) { + expect(listing).toContain(stack.id); + + function validateDependencies(dependencies: DependencyDetails[]) { + for (const dependency of dependencies) { + expect(listing).toContain(dependency.id); + if (dependency.dependencies.length > 0) { + validateDependencies(dependency.dependencies); + } + } + } + + if (stack.dependencies.length > 0) { + validateDependencies(stack.dependencies); + } + } + + for (const stack of expectedStacks) { + validateStackDependencies(stack); + } + }), +); + +/** + * Type to store stack dependencies recursively + */ +type DependencyDetails = { + id: string; + dependencies: DependencyDetails[]; +}; + +type StackDetails = { + id: string; + dependencies: DependencyDetails[]; +}; diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-ls.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-ls.integtest.ts new file mode 100644 index 000000000..29f4702b8 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-ls.integtest.ts @@ -0,0 +1,36 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk ls', + withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls'], { captureStderr: false }); + + const expectedStacks = [ + 'conditional-resource', + 'docker', + 'docker-with-custom-file', + 'failed', + 'iam-test', + 'lambda', + 'missing-ssm-parameter', + 'order-providing', + 'outputs-test-1', + 'outputs-test-2', + 'param-test-1', + 'param-test-2', + 'param-test-3', + 'termination-protection', + 'test-1', + 'test-2', + 'with-nested-stack', + 'with-nested-stack-using-parameters', + 'order-consuming', + ]; + + for (const stack of expectedStacks) { + expect(listing).toContain(fixture.fullStackName(stack)); + } + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-notices-are-displayed-correctly.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-notices-are-displayed-correctly.integtest.ts new file mode 100644 index 000000000..a9aa10308 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-notices-are-displayed-correctly.integtest.ts @@ -0,0 +1,42 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('cdk notices are displayed correctly', withDefaultFixture(async (fixture) => { + const cache = { + expiration: 4125963264000, // year 2100 so we never overwrite the cache + notices: [ + { + title: 'Bootstrap 1999 Notice', + issueNumber: 4444, + overview: 'Overview for Bootstrap 1999 Notice. AffectedEnvironments:<{resolve:ENVIRONMENTS}>', + components: [ + { + name: 'bootstrap', + version: '<1999', // so we include all possible environments + }, + ], + schemaVersion: '1', + }, + ], + }; + + const cdkCacheDir = path.join(fixture.integTestDir, 'cache'); + await fs.mkdir(cdkCacheDir); + await fs.writeFile(path.join(cdkCacheDir, 'notices.json'), JSON.stringify(cache)); + + const output = await fixture.cdkDeploy('notices', { + verbose: false, + modEnv: { + CDK_HOME: fixture.integTestDir, + }, + }); + + expect(output).toContain('Overview for Bootstrap 1999 Notice'); + + // assert dynamic environments are resolved + expect(output).toContain(`AffectedEnvironments:`); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-notices-with---unacknowledged.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-notices-with---unacknowledged.integtest.ts new file mode 100644 index 000000000..61c1bac81 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-notices-with---unacknowledged.integtest.ts @@ -0,0 +1,14 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk notices with --unacknowledged', + withDefaultFixture(async (fixture) => { + const noticesUnacknowledged = await fixture.cdk(['notices', '--unacknowledged'], { verbose: false }); + const noticesUnacknowledgedAlias = await fixture.cdk(['notices', '-u'], { verbose: false }); + expect(noticesUnacknowledged).toEqual(expect.stringMatching(/There are \d{1,} unacknowledged notice\(s\)./)); + expect(noticesUnacknowledged).toEqual(noticesUnacknowledgedAlias); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-synth-add-the-metadata-properties-expected-by-sam.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-synth-add-the-metadata-properties-expected-by-sam.integtest.ts new file mode 100644 index 000000000..985117552 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-synth-add-the-metadata-properties-expected-by-sam.integtest.ts @@ -0,0 +1,126 @@ +import { integTest, withSamIntegrationFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'CDK synth add the metadata properties expected by sam', + withSamIntegrationFixture(async (fixture) => { + // Synth first + await fixture.cdkSynth(); + + const template = fixture.template('TestStack'); + + const expectedResources = [ + { + // Python Layer Version + id: 'PythonLayerVersion39495CEF', + cdkId: 'PythonLayerVersion', + isBundled: true, + property: 'Content', + }, + { + // Layer Version + id: 'LayerVersion3878DA3A', + cdkId: 'LayerVersion', + isBundled: false, + property: 'Content', + }, + { + // Bundled layer version + id: 'BundledLayerVersionPythonRuntime6BADBD6E', + cdkId: 'BundledLayerVersionPythonRuntime', + isBundled: true, + property: 'Content', + }, + { + // Python Function + id: 'PythonFunction0BCF77FD', + cdkId: 'PythonFunction', + isBundled: true, + property: 'Code', + }, + { + // Log Retention Function + id: 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aFD4BFC8A', + cdkId: 'LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a', + isBundled: false, + property: 'Code', + }, + { + // Function + id: 'FunctionPythonRuntime28CBDA05', + cdkId: 'FunctionPythonRuntime', + isBundled: false, + property: 'Code', + }, + { + // Bundled Function + id: 'BundledFunctionPythonRuntime4D9A0918', + cdkId: 'BundledFunctionPythonRuntime', + isBundled: true, + property: 'Code', + }, + { + // NodeJs Function + id: 'NodejsFunction09C1F20F', + cdkId: 'NodejsFunction', + isBundled: true, + property: 'Code', + }, + { + // Go Function + id: 'GoFunctionCA95FBAA', + cdkId: 'GoFunction', + isBundled: true, + property: 'Code', + }, + { + // Docker Image Function + id: 'DockerImageFunction28B773E6', + cdkId: 'DockerImageFunction', + dockerFilePath: 'Dockerfile', + property: 'Code.ImageUri', + }, + { + // Spec Rest Api + id: 'SpecRestAPI7D4B3A34', + cdkId: 'SpecRestAPI', + property: 'BodyS3Location', + }, + ]; + + for (const resource of expectedResources) { + fixture.output.write(`validate assets metadata for resource ${resource}`); + expect(resource.id in template.Resources).toBeTruthy(); + expect(template.Resources[resource.id]).toEqual( + expect.objectContaining({ + Metadata: { + 'aws:cdk:path': `${fixture.fullStackName('TestStack')}/${resource.cdkId}/Resource`, + 'aws:asset:path': expect.stringMatching(/asset\.[0-9a-zA-Z]{64}/), + 'aws:asset:is-bundled': resource.isBundled, + 'aws:asset:dockerfile-path': resource.dockerFilePath, + 'aws:asset:property': resource.property, + }, + }), + ); + } + + // Nested Stack + fixture.output.write('validate assets metadata for nested stack resource'); + expect('NestedStackNestedStackNestedStackNestedStackResourceB70834FD' in template.Resources).toBeTruthy(); + expect(template.Resources.NestedStackNestedStackNestedStackNestedStackResourceB70834FD).toEqual( + expect.objectContaining({ + Metadata: { + 'aws:cdk:path': `${fixture.fullStackName( + 'TestStack', + )}/NestedStack.NestedStack/NestedStack.NestedStackResource`, + 'aws:asset:path': expect.stringMatching( + `${fixture.stackNamePrefix.replace(/-/, '')}TestStackNestedStack[0-9A-Z]{8}\.nested\.template\.json`, + ), + 'aws:asset:property': 'TemplateURL', + }, + }), + ); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-synth-bundled-functions-as-expected.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-synth-bundled-functions-as-expected.integtest.ts new file mode 100644 index 000000000..b177bfcab --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-synth-bundled-functions-as-expected.integtest.ts @@ -0,0 +1,80 @@ +import { existsSync } from 'fs'; +import * as path from 'path'; +import { integTest, withSamIntegrationFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'CDK synth bundled functions as expected', + withSamIntegrationFixture(async (fixture) => { + // Synth first + await fixture.cdkSynth(); + + const template = fixture.template('TestStack'); + + const expectedBundledAssets = [ + { + // Python Layer Version + id: 'PythonLayerVersion39495CEF', + files: [ + 'python/layer_version_dependency.py', + 'python/geonamescache/__init__.py', + 'python/geonamescache-1.3.0.dist-info', + ], + }, + { + // Layer Version + id: 'LayerVersion3878DA3A', + files: ['layer_version_dependency.py', 'requirements.txt'], + }, + { + // Bundled layer version + id: 'BundledLayerVersionPythonRuntime6BADBD6E', + files: [ + 'python/layer_version_dependency.py', + 'python/geonamescache/__init__.py', + 'python/geonamescache-1.3.0.dist-info', + ], + }, + { + // Python Function + id: 'PythonFunction0BCF77FD', + files: ['app.py', 'geonamescache/__init__.py', 'geonamescache-1.3.0.dist-info'], + }, + { + // Function + id: 'FunctionPythonRuntime28CBDA05', + files: ['app.py', 'requirements.txt'], + }, + { + // Bundled Function + id: 'BundledFunctionPythonRuntime4D9A0918', + files: ['app.py', 'geonamescache/__init__.py', 'geonamescache-1.3.0.dist-info'], + }, + { + // NodeJs Function + id: 'NodejsFunction09C1F20F', + files: ['index.js'], + }, + { + // Go Function + id: 'GoFunctionCA95FBAA', + files: ['bootstrap'], + }, + { + // Docker Image Function + id: 'DockerImageFunction28B773E6', + files: ['app.js', 'Dockerfile', 'package.json'], + }, + ]; + + for (const resource of expectedBundledAssets) { + const assetPath = template.Resources[resource.id].Metadata['aws:asset:path']; + for (const file of resource.files) { + fixture.output.write(`validate Path ${file} for resource ${resource}`); + expect(existsSync(path.join(fixture.integTestDir, 'cdk.out', assetPath, file))).toBeTruthy(); + } + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-synth.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-synth.integtest.ts new file mode 100644 index 000000000..66d4630b0 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-synth.integtest.ts @@ -0,0 +1,53 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk synth', + withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', fixture.fullStackName('test-1')]); + expect(fixture.template('test-1')).toEqual( + expect.objectContaining({ + Resources: { + topic69831491: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-1/topic/Resource`, + }, + }, + }, + }), + ); + + expect( + await fixture.cdkSynth({ + options: [fixture.fullStackName('test-1')], + }), + ).not.toEqual( + expect.stringContaining(` +Rules: + CheckBootstrapVersion:`), + ); + + await fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false }); + expect(fixture.template('test-2')).toEqual( + expect.objectContaining({ + Resources: { + topic152D84A37: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic1/Resource`, + }, + }, + topic2A4FB547F: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic2/Resource`, + }, + }, + }, + }), + ); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-ci-output-to-stderr.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-ci-output-to-stderr.integtest.ts new file mode 100644 index 000000000..7632ebe2f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-ci-output-to-stderr.integtest.ts @@ -0,0 +1,19 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'ci output to stderr', + withDefaultFixture(async (fixture) => { + const deployOutput = await fixture.cdkDeploy('test-2', { captureStderr: true, onlyStderr: true }); + const diffOutput = await fixture.cdk(['diff', fixture.fullStackName('test-2')], { + captureStderr: true, + onlyStderr: true, + }); + const destroyOutput = await fixture.cdkDestroy('test-2', { captureStderr: true, onlyStderr: true }); + expect(deployOutput).not.toEqual(''); + expect(destroyOutput).not.toEqual(''); + expect(diffOutput).not.toEqual(''); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-ci-true-output-to-stdout.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-ci-true-output-to-stdout.integtest.ts new file mode 100644 index 000000000..532caea70 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-ci-true-output-to-stdout.integtest.ts @@ -0,0 +1,29 @@ +import type { CdkCliOptions } from '../../lib'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'ci=true output to stdout', + withDefaultFixture(async (fixture) => { + const execOptions: CdkCliOptions = { + captureStderr: true, + onlyStderr: true, + modEnv: { + CI: 'true', + JSII_SILENCE_WARNING_KNOWN_BROKEN_NODE_VERSION: 'true', + JSII_SILENCE_WARNING_UNTESTED_NODE_VERSION: 'true', + JSII_SILENCE_WARNING_DEPRECATED_NODE_VERSION: 'true', + }, + options: ['--no-notices'], + }; + + const deployOutput = await fixture.cdkDeploy('test-2', execOptions); + const diffOutput = await fixture.cdk(['diff', '--no-notices', fixture.fullStackName('test-2')], execOptions); + const destroyOutput = await fixture.cdkDestroy('test-2', execOptions); + expect(deployOutput).toEqual(''); + expect(destroyOutput).toEqual(''); + expect(diffOutput).toEqual(''); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-construct-with-builtin-lambda-function.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-construct-with-builtin-lambda-function.integtest.ts new file mode 100644 index 000000000..dc69b6846 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-construct-with-builtin-lambda-function.integtest.ts @@ -0,0 +1,14 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Construct with builtin Lambda function', + withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('builtin-lambda-function'); + fixture.log('Setup complete!'); + await fixture.cdkDestroy('builtin-lambda-function'); + }), +); + +// this is to ensure that asset bundling for apps under a stage does not break diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-context-in-stage-propagates-to-top.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-context-in-stage-propagates-to-top.integtest.ts new file mode 100644 index 000000000..99a90910a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-context-in-stage-propagates-to-top.integtest.ts @@ -0,0 +1,20 @@ +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'context in stage propagates to top', + withoutBootstrap(async (fixture) => { + await expect( + fixture.cdkSynth({ + // This will make it error to prove that the context bubbles up, and also that we can fail on command + options: ['--no-lookups'], + modEnv: { + INTEG_STACK_SET: 'stage-using-context', + }, + allowErrExit: true, + }), + ).resolves.toContain('Context lookups have been disabled'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-context-setting.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-context-setting.integtest.ts new file mode 100644 index 000000000..97c61aeb7 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-context-setting.integtest.ts @@ -0,0 +1,33 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'context setting', + withDefaultFixture(async (fixture) => { + await fs.writeFile( + path.join(fixture.integTestDir, 'cdk.context.json'), + JSON.stringify({ + contextkey: 'this is the context value', + }), + ); + try { + await expect(fixture.cdk(['context'])).resolves.toContain('this is the context value'); + + // Test that deleting the contextkey works + await fixture.cdk(['context', '--reset', 'contextkey']); + await expect(fixture.cdk(['context'])).resolves.not.toContain('this is the context value'); + + // Test that forced delete of the context key does not throw + await fixture.cdk(['context', '-f', '--reset', 'contextkey']); + } finally { + await fs.unlink(path.join(fixture.integTestDir, 'cdk.context.json')); + } + }), +); + +// bootstrapping also performs synthesis. As it turns out, bootstrap-stage synthesis still causes the lookups to be cached, meaning that the lookup never +// happens when we actually call `cdk synth --no-lookups`. This results in the error never being thrown, because it never tries to lookup anything. +// Fix this by not trying to bootstrap; there's no need to bootstrap anyway, since the test never tries to deploy anything. diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy---method-direct.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy---method-direct.integtest.ts new file mode 100644 index 000000000..eaf10e3c2 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy---method-direct.integtest.ts @@ -0,0 +1,23 @@ +import { DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy --method=direct', + withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--method=direct'], + captureStderr: false, + }); + + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation.send( + new DescribeStackResourcesCommand({ + StackName: stackArn, + }), + ); + expect(response.StackResources?.length).toBeGreaterThan(0); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-all-concurrently.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-all-concurrently.integtest.ts new file mode 100644 index 000000000..c1d8841f7 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-all-concurrently.integtest.ts @@ -0,0 +1,17 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy all concurrently', + withDefaultFixture(async (fixture) => { + const arns = await fixture.cdkDeploy('test-*', { + captureStderr: false, + options: ['--concurrency', '2'], + }); + + // verify that we only deployed both stacks (there are 2 ARNs in the output) + expect(arns.split('\n').length).toEqual(2); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-all.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-all.integtest.ts new file mode 100644 index 000000000..66346bc21 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-all.integtest.ts @@ -0,0 +1,14 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy all', + withDefaultFixture(async (fixture) => { + const arns = await fixture.cdkDeploy('test-*', { captureStderr: false }); + + // verify that we only deployed both stacks (there are 2 ARNs in the output) + expect(arns.split('\n').length).toEqual(2); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-and-test-stack-with-lambda-asset.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-and-test-stack-with-lambda-asset.integtest.ts new file mode 100644 index 000000000..0cb22aa7f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-and-test-stack-with-lambda-asset.integtest.ts @@ -0,0 +1,31 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { InvokeCommand } from '@aws-sdk/client-lambda'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy and test stack with lambda asset', + withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('lambda', { captureStderr: false }); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + const lambdaArn = response.Stacks?.[0].Outputs?.[0].OutputValue; + if (lambdaArn === undefined) { + throw new Error('Stack did not have expected Lambda ARN output'); + } + + const output = await fixture.aws.lambda.send( + new InvokeCommand({ + FunctionName: lambdaArn, + }), + ); + + expect(JSON.stringify(output.Payload?.transformToString())).toContain('dear asset'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-deletes-all-notification-arns-when-empty-array-is-passed.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-deletes-all-notification-arns-when-empty-array-is-passed.integtest.ts new file mode 100644 index 000000000..eec8493f1 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-deletes-all-notification-arns-when-empty-array-is-passed.integtest.ts @@ -0,0 +1,50 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { CreateTopicCommand, DeleteTopicCommand } from '@aws-sdk/client-sns'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('deploy deletes ALL notification arns when empty array is passed', withDefaultFixture(async (fixture) => { + const topicName = `${fixture.stackNamePrefix}-topic`; + + const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName })); + const topicArn = response.TopicArn!; + + try { + await fixture.cdkDeploy('notification-arns', { + modEnv: { + INTEG_NOTIFICATION_ARNS: topicArn, + }, + }); + + // make sure the arn was added + let describeResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: fixture.fullStackName('notification-arns'), + }), + ); + expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]); + + // deploy again with empty array + await fixture.cdkDeploy('notification-arns', { + modEnv: { + INTEG_NOTIFICATION_ARNS: '', + }, + }); + + // make sure the arn was deleted + describeResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: fixture.fullStackName('notification-arns'), + }), + ); + expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([]); + } finally { + await fixture.aws.sns.send( + new DeleteTopicCommand({ + TopicArn: topicArn, + }), + ); + } +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-no-stacks-error.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-no-stacks-error.integtest.ts new file mode 100644 index 000000000..217f54dd2 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-no-stacks-error.integtest.ts @@ -0,0 +1,18 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy no stacks error', + withDefaultFixture(async (fixture) => { + // empty array for stack names + await expect( + fixture.cdkDeploy([], { + modEnv: { + INTEG_STACK_SET: 'stage-with-no-stacks', + }, + }), + ).rejects.toThrow('exited with error'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-no-stacks-with---ignore-no-stacks.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-no-stacks-with---ignore-no-stacks.integtest.ts new file mode 100644 index 000000000..29ead570d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-no-stacks-with---ignore-no-stacks.integtest.ts @@ -0,0 +1,17 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy no stacks with --ignore-no-stacks', + withDefaultFixture(async (fixture) => { + // empty array for stack names + await fixture.cdkDeploy([], { + options: ['--ignore-no-stacks'], + modEnv: { + INTEG_STACK_SET: 'stage-with-no-stacks', + }, + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-preserves-existing-notification-arns-when-not-specified.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-preserves-existing-notification-arns-when-not-specified.integtest.ts new file mode 100644 index 000000000..5331514eb --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-preserves-existing-notification-arns-when-not-specified.integtest.ts @@ -0,0 +1,51 @@ +import { DescribeStacksCommand, UpdateStackCommand, waitUntilStackUpdateComplete } from '@aws-sdk/client-cloudformation'; +import { CreateTopicCommand, DeleteTopicCommand } from '@aws-sdk/client-sns'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('deploy preserves existing notification arns when not specified', withDefaultFixture(async (fixture) => { + const topicName = `${fixture.stackNamePrefix}-topic`; + + const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName })); + const topicArn = response.TopicArn!; + + try { + await fixture.cdkDeploy('notification-arns'); + + // add notification arns externally to cdk + await fixture.aws.cloudFormation.send( + new UpdateStackCommand({ + StackName: fixture.fullStackName('notification-arns'), + UsePreviousTemplate: true, + NotificationARNs: [topicArn], + }), + ); + + await waitUntilStackUpdateComplete( + { + client: fixture.aws.cloudFormation, + maxWaitTime: 600, + }, + { StackName: fixture.fullStackName('notification-arns') }, + ); + + // deploy again + await fixture.cdkDeploy('notification-arns'); + + // make sure the notification arn is preserved + const describeResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: fixture.fullStackName('notification-arns'), + }), + ); + expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]); + } finally { + await fixture.aws.sns.send( + new DeleteTopicCommand({ + TopicArn: topicArn, + }), + ); + } +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-stack-with-docker-asset.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-stack-with-docker-asset.integtest.ts new file mode 100644 index 000000000..0e4053b7e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-stack-with-docker-asset.integtest.ts @@ -0,0 +1,11 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy stack with docker asset', + withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('docker'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-stack-with-lambda-asset-to-object-lock-enabled-asset-bucket.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-stack-with-lambda-asset-to-object-lock-enabled-asset-bucket.integtest.ts new file mode 100644 index 000000000..5886846c9 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-stack-with-lambda-asset-to-object-lock-enabled-asset-bucket.integtest.ts @@ -0,0 +1,42 @@ +import { PutObjectLockConfigurationCommand } from '@aws-sdk/client-s3'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('deploy stack with Lambda Asset to Object Lock-enabled asset bucket', withoutBootstrap(async (fixture) => { + // Bootstrapping with custom toolkit stack name and qualifier + const qualifier = fixture.qualifier; + const toolkitStackName = fixture.bootstrapStackName; + await fixture.cdkBootstrapModern({ + verbose: true, + toolkitStackName: toolkitStackName, + qualifier: qualifier, + }); + + const bucketName = `cdk-${qualifier}-assets-${await fixture.aws.account()}-${fixture.aws.region}`; + await fixture.aws.s3.send(new PutObjectLockConfigurationCommand({ + Bucket: bucketName, + ObjectLockConfiguration: { + ObjectLockEnabled: 'Enabled', + Rule: { + DefaultRetention: { + Days: 1, + Mode: 'GOVERNANCE', + }, + }, + }, + })); + + // Deploy a stack that definitely contains a file asset + await fixture.cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', toolkitStackName, + '--context', `@aws-cdk/core:bootstrapQualifier=${qualifier}`, + ], + }); + + // THEN - should not fail. Now clean the bucket with governance bypass: a regular delete + // operation will fail. + await fixture.aws.emptyBucket(bucketName, { bypassGovernance: true }); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-stack-without-resource.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-stack-without-resource.integtest.ts new file mode 100644 index 000000000..cc3a4da7a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-stack-without-resource.integtest.ts @@ -0,0 +1,32 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy stack without resource', + withDefaultFixture(async (fixture) => { + // Deploy the stack without resources + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + + // This should have succeeded but not deployed the stack. + await expect( + fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ StackName: fixture.fullStackName('conditional-resource') }), + ), + ).rejects.toThrow('conditional-resource does not exist'); + + // Deploy the stack with resources + await fixture.cdkDeploy('conditional-resource'); + + // Then again WITHOUT resources (this should destroy the stack) + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + + await expect( + fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ StackName: fixture.fullStackName('conditional-resource') }), + ), + ).rejects.toThrow('conditional-resource does not exist'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-wildcard-with-outputs.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-wildcard-with-outputs.integtest.ts new file mode 100644 index 000000000..bed7237df --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-wildcard-with-outputs.integtest.ts @@ -0,0 +1,28 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy wildcard with outputs', + withDefaultFixture(async (fixture) => { + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs.mkdir(path.dirname(outputsFile), { recursive: true }); + + await fixture.cdkDeploy(['outputs-test-*'], { + options: ['--outputs-file', outputsFile], + }); + + const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + expect(outputs).toEqual({ + [`${fixture.stackNamePrefix}-outputs-test-1`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-1MyTopic`, + }, + [`${fixture.stackNamePrefix}-outputs-test-2`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-2MyOtherTopic`, + }, + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-import-existing-resources-true.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-import-existing-resources-true.integtest.ts new file mode 100644 index 000000000..a6932b6cf --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-import-existing-resources-true.integtest.ts @@ -0,0 +1,29 @@ +import { DescribeStacksCommand, ListChangeSetsCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('deploy with import-existing-resources true', withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--no-execute', '--import-existing-resources'], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + + const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ + StackName: stackArn, + })); + expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); + + // verify a change set was successfully created + // Here, we do not test whether a resource is actually imported, because that is a CloudFormation feature, not a CDK feature. + const changeSetResponse = await fixture.aws.cloudFormation.send(new ListChangeSetsCommand({ + StackName: stackArn, + })); + const changeSets = changeSetResponse.Summaries || []; + expect(changeSets.length).toEqual(1); + expect(changeSets[0].Status).toEqual('CREATE_COMPLETE'); + expect(changeSets[0].ImportExistingResources).toEqual(true); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-method-direct-and-import-existing-resources-fails.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-method-direct-and-import-existing-resources-fails.integtest.ts new file mode 100644 index 000000000..284904dc5 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-method-direct-and-import-existing-resources-fails.integtest.ts @@ -0,0 +1,17 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('deploy with method=direct and import-existing-resources fails', withDefaultFixture(async (fixture) => { + const stackName = 'iam-test'; + await expect(fixture.cdkDeploy(stackName, { + options: ['--import-existing-resources', '--method=direct'], + })).rejects.toThrow('exited with error'); + + // Ensure stack was not deployed + await expect(fixture.aws.cloudFormation.send(new DescribeStacksCommand({ + StackName: fixture.fullStackName(stackName), + }))).rejects.toThrow('does not exist'); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-notification-arn-as-flag.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-notification-arn-as-flag.integtest.ts new file mode 100644 index 000000000..bb80db98f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-notification-arn-as-flag.integtest.ts @@ -0,0 +1,36 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { CreateTopicCommand, DeleteTopicCommand } from '@aws-sdk/client-sns'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy with notification ARN as flag', + withDefaultFixture(async (fixture) => { + const topicName = `${fixture.stackNamePrefix}-test-topic-flag`; + + const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName })); + const topicArn = response.TopicArn!; + + try { + await fixture.cdkDeploy('notification-arns', { + options: ['--notification-arns', topicArn], + }); + + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: fixture.fullStackName('notification-arns'), + }), + ); + expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]); + } finally { + await fixture.aws.sns.send( + new DeleteTopicCommand({ + TopicArn: topicArn, + }), + ); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-notification-arn-as-prop-and-flag.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-notification-arn-as-prop-and-flag.integtest.ts new file mode 100644 index 000000000..a92d6599a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-notification-arn-as-prop-and-flag.integtest.ts @@ -0,0 +1,45 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { CreateTopicCommand, DeleteTopicCommand } from '@aws-sdk/client-sns'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('deploy with notification ARN as prop and flag', withDefaultFixture(async (fixture) => { + const topic1Name = `${fixture.stackNamePrefix}-topic1`; + const topic2Name = `${fixture.stackNamePrefix}-topic1`; + + const topic1Arn = (await fixture.aws.sns.send(new CreateTopicCommand({ Name: topic1Name }))).TopicArn!; + const topic2Arn = (await fixture.aws.sns.send(new CreateTopicCommand({ Name: topic2Name }))).TopicArn!; + + try { + await fixture.cdkDeploy('notification-arns', { + modEnv: { + INTEG_NOTIFICATION_ARNS: topic1Arn, + + }, + options: ['--notification-arns', topic2Arn], + }); + + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: fixture.fullStackName('notification-arns'), + }), + ); + expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topic1Arn, topic2Arn]); + } finally { + await fixture.aws.sns.send( + new DeleteTopicCommand({ + TopicArn: topic1Arn, + }), + ); + await fixture.aws.sns.send( + new DeleteTopicCommand({ + TopicArn: topic2Arn, + }), + ); + } +})); + +// NOTE: this doesn't currently work with modern-style synthesis, as the bootstrap +// role by default will not have permission to iam:PassRole the created role. diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-notification-arn-as-prop.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-notification-arn-as-prop.integtest.ts new file mode 100644 index 000000000..ce12b700c --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-notification-arn-as-prop.integtest.ts @@ -0,0 +1,37 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { CreateTopicCommand, DeleteTopicCommand } from '@aws-sdk/client-sns'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('deploy with notification ARN as prop', withDefaultFixture(async (fixture) => { + const topicName = `${fixture.stackNamePrefix}-test-topic-prop`; + + const response = await fixture.aws.sns.send(new CreateTopicCommand({ Name: topicName })); + const topicArn = response.TopicArn!; + + try { + await fixture.cdkDeploy('notification-arns', { + modEnv: { + INTEG_NOTIFICATION_ARNS: topicArn, + + }, + }); + + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: fixture.fullStackName('notification-arns'), + }), + ); + expect(describeResponse.Stacks?.[0].NotificationARNs).toEqual([topicArn]); + } finally { + await fixture.aws.sns.send( + new DeleteTopicCommand({ + TopicArn: topicArn, + }), + ); + } +})); + +// https://github.com/aws/aws-cdk/issues/32153 diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-parameters-multi.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-parameters-multi.integtest.ts new file mode 100644 index 000000000..bd6efd412 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-parameters-multi.integtest.ts @@ -0,0 +1,33 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy with parameters multi', + withDefaultFixture(async (fixture) => { + const paramVal1 = `${fixture.stackNamePrefix}bazinga`; + const paramVal2 = `${fixture.stackNamePrefix}=jagshemash`; + + const stackArn = await fixture.cdkDeploy('param-test-3', { + options: ['--parameters', `DisplayNameParam=${paramVal1}`, '--parameters', `OtherDisplayNameParam=${paramVal2}`], + captureStderr: false, + }); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + + expect(response.Stacks?.[0].Parameters).toContainEqual({ + ParameterKey: 'DisplayNameParam', + ParameterValue: paramVal1, + }); + expect(response.Stacks?.[0].Parameters).toContainEqual({ + ParameterKey: 'OtherDisplayNameParam', + ParameterValue: paramVal2, + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-parameters.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-parameters.integtest.ts new file mode 100644 index 000000000..85f989185 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-parameters.integtest.ts @@ -0,0 +1,26 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy with parameters', + withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}bazinga`], + captureStderr: false, + }); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + + expect(response.Stacks?.[0].Parameters).toContainEqual({ + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}bazinga`, + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-role.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-role.integtest.ts new file mode 100644 index 000000000..a73100bae --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-role.integtest.ts @@ -0,0 +1,119 @@ +import { CreateRoleCommand, DeleteRoleCommand, DeleteRolePolicyCommand, ListRolePoliciesCommand, PutRolePolicyCommand } from '@aws-sdk/client-iam'; +import { AssumeRoleCommand, GetCallerIdentityCommand } from '@aws-sdk/client-sts'; +import { integTest, retry, withDefaultFixture, sleep } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy with role', + withDefaultFixture(async (fixture) => { + if (fixture.packages.majorVersion() !== '1') { + return; // Nothing to do + } + + const roleName = `${fixture.stackNamePrefix}-test-role`; + + await deleteRole(); + + const createResponse = await fixture.aws.iam.send( + new CreateRoleCommand({ + RoleName: roleName, + AssumeRolePolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Action: 'sts:AssumeRole', + Principal: { Service: 'cloudformation.amazonaws.com' }, + Effect: 'Allow', + }, + { + Action: 'sts:AssumeRole', + Principal: { AWS: (await fixture.aws.sts.send(new GetCallerIdentityCommand({}))).Arn }, + Effect: 'Allow', + }, + ], + }), + }), + ); + + if (!createResponse.Role) { + throw new Error('Role is expected to be present!!'); + } + + if (!createResponse.Role.Arn) { + throw new Error('Role arn is expected to be present!!'); + } + + const roleArn = createResponse.Role.Arn; + try { + await fixture.aws.iam.send( + new PutRolePolicyCommand({ + RoleName: roleName, + PolicyName: 'DefaultPolicy', + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Action: '*', + Resource: '*', + Effect: 'Allow', + }, + ], + }), + }), + ); + + await retry(fixture.output, 'Trying to assume fresh role', retry.forSeconds(300), async () => { + await fixture.aws.sts.send( + new AssumeRoleCommand({ + RoleArn: roleArn, + RoleSessionName: 'testing', + }), + ); + }); + + // In principle, the role has replicated from 'us-east-1' to wherever we're testing. + // Give it a little more sleep to make sure CloudFormation is not hitting a box + // that doesn't have it yet. + await sleep(5000); + + await fixture.cdkDeploy('test-2', { + options: ['--role-arn', roleArn], + }); + + // Immediately delete the stack again before we delete the role. + // + // Since roles are sticky, if we delete the role before the stack, subsequent DeleteStack + // operations will fail when CloudFormation tries to assume the role that's already gone. + await fixture.cdkDestroy('test-2'); + } finally { + await deleteRole(); + } + + async function deleteRole() { + try { + const response = await fixture.aws.iam.send(new ListRolePoliciesCommand({ RoleName: roleName })); + + if (!response.PolicyNames) { + throw new Error('Policy names cannot be undefined for deleteRole() function'); + } + + for (const policyName of response.PolicyNames) { + await fixture.aws.iam.send( + new DeleteRolePolicyCommand({ + RoleName: roleName, + PolicyName: policyName, + }), + ); + } + await fixture.aws.iam.send(new DeleteRoleCommand({ RoleName: roleName })); + } catch (e: any) { + if (e.message.indexOf('cannot be found') > -1) { + return; + } + throw e; + } + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-wildcard-and-parameters.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-wildcard-and-parameters.integtest.ts new file mode 100644 index 000000000..fce74a2ff --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-with-wildcard-and-parameters.integtest.ts @@ -0,0 +1,22 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy with wildcard and parameters', + withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('param-test-*', { + options: [ + '--parameters', + `${fixture.stackNamePrefix}-param-test-1:TopicNameParam=${fixture.stackNamePrefix}bazinga`, + '--parameters', + `${fixture.stackNamePrefix}-param-test-2:OtherTopicNameParam=${fixture.stackNamePrefix}ThatsMySpot`, + '--parameters', + `${fixture.stackNamePrefix}-param-test-3:DisplayNameParam=${fixture.stackNamePrefix}HeyThere`, + '--parameters', + `${fixture.stackNamePrefix}-param-test-3:OtherDisplayNameParam=${fixture.stackNamePrefix}AnotherOne`, + ], + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-without-execute-a-named-change-set.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-without-execute-a-named-change-set.integtest.ts new file mode 100644 index 000000000..8dbc00919 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-without-execute-a-named-change-set.integtest.ts @@ -0,0 +1,36 @@ +import { DescribeStacksCommand, ListChangeSetsCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy without execute a named change set', + withDefaultFixture(async (fixture) => { + const changeSetName = 'custom-change-set-name'; + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--no-execute', '--change-set-name', changeSetName], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); + + // verify a change set was created with the provided name + const changeSetResponse = await fixture.aws.cloudFormation.send( + new ListChangeSetsCommand({ + StackName: stackArn, + }), + ); + const changeSets = changeSetResponse.Summaries || []; + expect(changeSets.length).toEqual(1); + expect(changeSets[0].ChangeSetName).toEqual(changeSetName); + expect(changeSets[0].Status).toEqual('CREATE_COMPLETE'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-without-import-existing-resources.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-without-import-existing-resources.integtest.ts new file mode 100644 index 000000000..e49531bbd --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy-without-import-existing-resources.integtest.ts @@ -0,0 +1,28 @@ +import { DescribeStacksCommand, ListChangeSetsCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('deploy without import-existing-resources', withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--no-execute'], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + + const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ + StackName: stackArn, + })); + expect(response.Stacks?.[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); + + // verify a change set was successfully created and ImportExistingResources = false + const changeSetResponse = await fixture.aws.cloudFormation.send(new ListChangeSetsCommand({ + StackName: stackArn, + })); + const changeSets = changeSetResponse.Summaries || []; + expect(changeSets.length).toEqual(1); + expect(changeSets[0].Status).toEqual('CREATE_COMPLETE'); + expect(changeSets[0].ImportExistingResources).toEqual(false); +})); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy.integtest.ts new file mode 100644 index 000000000..1c15539c4 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-deploy.integtest.ts @@ -0,0 +1,20 @@ +import { DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'deploy', + withDefaultFixture(async (fixture) => { + const stackArn = await fixture.cdkDeploy('test-2', { captureStderr: false }); + + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation.send( + new DescribeStackResourcesCommand({ + StackName: stackArn, + }), + ); + expect(response.StackResources?.length).toEqual(2); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-destroy-interactive.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-destroy-interactive.integtest.ts new file mode 100644 index 000000000..97eb1f981 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-destroy-interactive.integtest.ts @@ -0,0 +1,34 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +integTest('cdk destroy prompts the user for confirmation', withDefaultFixture(async (fixture) => { + const stackName = 'test-2'; + const fullStackName = fixture.fullStackName(stackName); + + fixture.log(`Deploying stack ${fullStackName}`); + await fixture.cdkDeploy(stackName); + + fixture.log(`Destroying stack ${fullStackName} and declining prompt`); + await fixture.cdkDestroy(stackName, { + force: false, + interact: [ + { prompt: /Are you sure you want to delete/, input: 'no' }, + ], + }); + + // assert we didn't destroy the stack + const stack = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: fullStackName })); + expect(stack.Stacks?.length ?? 0).toEqual(1); + + fixture.log(`Destroying stack ${fullStackName} and accepting prompt`); + await fixture.cdkDestroy(stackName, { + force: false, + interact: [ + { prompt: /Are you sure you want to delete/, input: 'yes' }, + ], + }); + + // assert we did destroy the stack + await expect(fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: fullStackName }))) + .rejects.toThrow(/does not exist/); +})); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-doubly-nested-stack.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-doubly-nested-stack.integtest.ts new file mode 100644 index 000000000..a404fc920 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-doubly-nested-stack.integtest.ts @@ -0,0 +1,12 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('doubly nested stack', + withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('with-doubly-nested-stack', { + captureStderr: false, + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-enablediffnofail.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-enablediffnofail.integtest.ts new file mode 100644 index 000000000..68f10133c --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-enablediffnofail.integtest.ts @@ -0,0 +1,44 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'enableDiffNoFail', + withDefaultFixture(async (fixture) => { + await diffShouldSucceedWith({ fail: false, enableDiffNoFail: false }); + await diffShouldSucceedWith({ fail: false, enableDiffNoFail: true }); + await diffShouldFailWith({ fail: true, enableDiffNoFail: false }); + await diffShouldFailWith({ fail: true, enableDiffNoFail: true }); + await diffShouldFailWith({ fail: undefined, enableDiffNoFail: false }); + await diffShouldSucceedWith({ fail: undefined, enableDiffNoFail: true }); + + async function diffShouldSucceedWith(props: DiffParameters) { + await expect(diff(props)).resolves.not.toThrow(); + } + + async function diffShouldFailWith(props: DiffParameters) { + await expect(diff(props)).rejects.toThrow('exited with error'); + } + + async function diff(props: DiffParameters): Promise { + await updateContext(props.enableDiffNoFail); + const flag = props.fail != null ? (props.fail ? '--fail' : '--no-fail') : ''; + + return fixture.cdk(['diff', flag, fixture.fullStackName('test-1')]); + } + + async function updateContext(enableDiffNoFail: boolean) { + const cdkJson = JSON.parse(await fs.readFile(path.join(fixture.integTestDir, 'cdk.json'), 'utf8')); + cdkJson.context = { + ...cdkJson.context, + 'aws-cdk:enableDiffNoFail': enableDiffNoFail, + }; + await fs.writeFile(path.join(fixture.integTestDir, 'cdk.json'), JSON.stringify(cdkJson)); + } + + type DiffParameters = { fail?: boolean; enableDiffNoFail: boolean }; + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-failed-deploy-does-not-hang.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-failed-deploy-does-not-hang.integtest.ts new file mode 100644 index 000000000..88d872b48 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-failed-deploy-does-not-hang.integtest.ts @@ -0,0 +1,12 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'failed deploy does not hang', + withDefaultFixture(async (fixture) => { + // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. + await expect(fixture.cdkDeploy('failed')).rejects.toThrow('exited with error'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-fast-deploy.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-fast-deploy.integtest.ts new file mode 100644 index 000000000..bd469d753 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-fast-deploy.integtest.ts @@ -0,0 +1,41 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'fast deploy', + withDefaultFixture(async (fixture) => { + // we are using a stack with a nested stack because CFN will always attempt to + // update a nested stack, which will allow us to verify that updates are actually + // skipped unless --force is specified. + const stackArn = await fixture.cdkDeploy('with-nested-stack', { captureStderr: false }); + const changeSet1 = await getLatestChangeSet(); + + // Deploy the same stack again, there should be no new change set created + await fixture.cdkDeploy('with-nested-stack'); + const changeSet2 = await getLatestChangeSet(); + expect(changeSet2.ChangeSetId).toEqual(changeSet1.ChangeSetId); + + // Deploy the stack again with --force, now we should create a changeset + await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] }); + const changeSet3 = await getLatestChangeSet(); + expect(changeSet3.ChangeSetId).not.toEqual(changeSet2.ChangeSetId); + + // Deploy the stack again with tags, expected to create a new changeset + // even though the resources didn't change. + await fixture.cdkDeploy('with-nested-stack', { options: ['--tags', 'key=value'] }); + const changeSet4 = await getLatestChangeSet(); + expect(changeSet4.ChangeSetId).not.toEqual(changeSet3.ChangeSetId); + + async function getLatestChangeSet() { + const response = await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: stackArn })); + if (!response.Stacks?.[0]) { + throw new Error('Did not get a ChangeSet at all'); + } + fixture.log(`Found Change Set ${response.Stacks?.[0].ChangeSetId}`); + return response.Stacks?.[0]; + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-deletes-unused-ecr-images.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-deletes-unused-ecr-images.integtest.ts new file mode 100644 index 000000000..6e9636254 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-deletes-unused-ecr-images.integtest.ts @@ -0,0 +1,48 @@ +import { ListImagesCommand } from '@aws-sdk/client-ecr'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Garbage Collection deletes unused ecr images', + withoutBootstrap(async (fixture) => { + const toolkitStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + toolkitStackName, + }); + + const repoName = await fixture.bootstrapRepoName(); + + await fixture.cdkDeploy('docker-in-use', { + options: [ + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + fixture.log('Setup complete!'); + + await fixture.cdkDestroy('docker-in-use', { + options: [ + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + + await fixture.cdkGarbageCollect({ + rollbackBufferDays: 0, + type: 'ecr', + bootstrapStackName: toolkitStackName, + }); + fixture.log('Garbage collection complete!'); + + // assert that the bootstrap repository is empty + await fixture.aws.ecr.send(new ListImagesCommand({ repositoryName: repoName })) + .then((result) => { + expect(result.imageIds).toEqual([]); + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-deletes-unused-s3-objects.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-deletes-unused-s3-objects.integtest.ts new file mode 100644 index 000000000..fb43ac6f2 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-deletes-unused-s3-objects.integtest.ts @@ -0,0 +1,51 @@ +import { ListObjectsV2Command } from '@aws-sdk/client-s3'; +import { integTest, withoutBootstrap, randomString } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Garbage Collection deletes unused s3 objects', + withoutBootstrap(async (fixture) => { + const toolkitStackName = fixture.bootstrapStackName; + const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`; + fixture.rememberToDeleteBucket(bootstrapBucketName); // just in case + + await fixture.cdkBootstrapModern({ + toolkitStackName, + bootstrapBucketName, + }); + + await fixture.cdkDeploy('lambda', { + options: [ + '--context', `bootstrapBucket=${bootstrapBucketName}`, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + fixture.log('Setup complete!'); + + await fixture.cdkDestroy('lambda', { + options: [ + '--context', `bootstrapBucket=${bootstrapBucketName}`, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + + await fixture.cdkGarbageCollect({ + rollbackBufferDays: 0, + type: 's3', + bootstrapStackName: toolkitStackName, + }); + fixture.log('Garbage collection complete!'); + + // assert that the bootstrap bucket is empty + await fixture.aws.s3.send(new ListObjectsV2Command({ Bucket: bootstrapBucketName })) + .then((result) => { + expect(result.Contents).toBeUndefined(); + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-keeps-in-use-ecr-images.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-keeps-in-use-ecr-images.integtest.ts new file mode 100644 index 000000000..31482ee5c --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-keeps-in-use-ecr-images.integtest.ts @@ -0,0 +1,48 @@ +import { ListImagesCommand } from '@aws-sdk/client-ecr'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Garbage Collection keeps in use ecr images', + withoutBootstrap(async (fixture) => { + const toolkitStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + toolkitStackName, + }); + + const repoName = await fixture.bootstrapRepoName(); + + await fixture.cdkDeploy('docker-in-use', { + options: [ + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + fixture.log('Setup complete!'); + + await fixture.cdkGarbageCollect({ + rollbackBufferDays: 0, + type: 'ecr', + bootstrapStackName: toolkitStackName, + }); + fixture.log('Garbage collection complete!'); + + // assert that the bootstrap repository is empty + await fixture.aws.ecr.send(new ListImagesCommand({ repositoryName: repoName })) + .then((result) => { + expect(result.imageIds).toHaveLength(1); + }); + + await fixture.cdkDestroy('docker-in-use', { + options: [ + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-keeps-in-use-s3-objects.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-keeps-in-use-s3-objects.integtest.ts new file mode 100644 index 000000000..d630d268e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-keeps-in-use-s3-objects.integtest.ts @@ -0,0 +1,52 @@ +import { ListObjectsV2Command } from '@aws-sdk/client-s3'; +import { integTest, withoutBootstrap, randomString } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Garbage Collection keeps in use s3 objects', + withoutBootstrap(async (fixture) => { + const toolkitStackName = fixture.bootstrapStackName; + const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`; + fixture.rememberToDeleteBucket(bootstrapBucketName); // just in case + + await fixture.cdkBootstrapModern({ + toolkitStackName, + bootstrapBucketName, + }); + + await fixture.cdkDeploy('lambda', { + options: [ + '--context', `bootstrapBucket=${bootstrapBucketName}`, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + fixture.log('Setup complete!'); + + await fixture.cdkGarbageCollect({ + rollbackBufferDays: 0, + type: 's3', + bootstrapStackName: toolkitStackName, + }); + fixture.log('Garbage collection complete!'); + + // assert that the bootstrap bucket has the object + await fixture.aws.s3.send(new ListObjectsV2Command({ Bucket: bootstrapBucketName })) + .then((result) => { + expect(result.Contents).toHaveLength(1); + }); + + await fixture.cdkDestroy('lambda', { + options: [ + '--context', `bootstrapBucket=${bootstrapBucketName}`, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + fixture.log('Teardown complete!'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-tags-unused-ecr-images.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-tags-unused-ecr-images.integtest.ts new file mode 100644 index 000000000..5e164bc36 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-tags-unused-ecr-images.integtest.ts @@ -0,0 +1,47 @@ +import { ListImagesCommand } from '@aws-sdk/client-ecr'; +import { integTest, withoutBootstrap } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Garbage Collection tags unused ecr images', + withoutBootstrap(async (fixture) => { + const toolkitStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + toolkitStackName, + }); + + const repoName = await fixture.bootstrapRepoName(); + + await fixture.cdkDeploy('docker-in-use', { + options: [ + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + fixture.log('Setup complete!'); + + await fixture.cdkDestroy('docker-in-use', { + options: [ + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + + await fixture.cdkGarbageCollect({ + rollbackBufferDays: 100, // this will ensure that we do not delete assets immediately (and just tag them) + type: 'ecr', + bootstrapStackName: toolkitStackName, + }); + fixture.log('Garbage collection complete!'); + + await fixture.aws.ecr.send(new ListImagesCommand({ repositoryName: repoName })) + .then((result) => { + expect(result.imageIds).toHaveLength(2); // the second tag comes in as a second 'id' + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-tags-unused-s3-objects.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-tags-unused-s3-objects.integtest.ts new file mode 100644 index 000000000..a971c5564 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-tags-unused-s3-objects.integtest.ts @@ -0,0 +1,63 @@ +import { GetObjectTaggingCommand, ListObjectsV2Command } from '@aws-sdk/client-s3'; +import { integTest, withoutBootstrap, randomString } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Garbage Collection tags unused s3 objects', + withoutBootstrap(async (fixture) => { + const toolkitStackName = fixture.bootstrapStackName; + const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`; + fixture.rememberToDeleteBucket(bootstrapBucketName); // just in case + + await fixture.cdkBootstrapModern({ + toolkitStackName, + bootstrapBucketName, + }); + + await fixture.cdkDeploy('lambda', { + options: [ + '--context', `bootstrapBucket=${bootstrapBucketName}`, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + fixture.log('Setup complete!'); + + await fixture.cdkDestroy('lambda', { + options: [ + '--context', `bootstrapBucket=${bootstrapBucketName}`, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + + await fixture.cdkGarbageCollect({ + rollbackBufferDays: 100, // this will ensure that we do not delete assets immediately (and just tag them) + type: 's3', + bootstrapStackName: toolkitStackName, + }); + fixture.log('Garbage collection complete!'); + + // assert that the bootstrap bucket has the object and is tagged + await fixture.aws.s3.send(new ListObjectsV2Command({ Bucket: bootstrapBucketName })) + .then(async (result) => { + expect(result.Contents).toHaveLength(2); // also the CFN template + const key = result.Contents![0].Key; + const tags = await fixture.aws.s3.send(new GetObjectTaggingCommand({ Bucket: bootstrapBucketName, Key: key })); + expect(tags.TagSet).toHaveLength(1); + }); + + await fixture.cdkDestroy('lambda', { + options: [ + '--context', `bootstrapBucket=${bootstrapBucketName}`, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-untags-in-use-ecr-images.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-untags-in-use-ecr-images.integtest.ts new file mode 100644 index 000000000..49248d986 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-untags-in-use-ecr-images.integtest.ts @@ -0,0 +1,55 @@ +import { BatchGetImageCommand, ListImagesCommand, PutImageCommand } from '@aws-sdk/client-ecr'; +import { integTest, withoutBootstrap } from '../../lib'; + +const ECR_ISOLATED_TAG = 'aws-cdk.isolated'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Garbage Collection untags in-use ecr images', + withoutBootstrap(async (fixture) => { + const toolkitStackName = fixture.bootstrapStackName; + + await fixture.cdkBootstrapModern({ + toolkitStackName, + }); + + const repoName = await fixture.bootstrapRepoName(); + + await fixture.cdkDeploy('docker-in-use', { + options: [ + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + fixture.log('Setup complete!'); + + // Artificially add tagging to the asset in the bootstrap bucket + const imageIds = await fixture.aws.ecr.send(new ListImagesCommand({ repositoryName: repoName })); + const digest = imageIds.imageIds![0].imageDigest; + const imageManifests = await fixture.aws.ecr.send(new BatchGetImageCommand({ repositoryName: repoName, imageIds: [{ imageDigest: digest }] })); + const manifest = imageManifests.images![0].imageManifest; + await fixture.aws.ecr.send(new PutImageCommand({ repositoryName: repoName, imageManifest: manifest, imageDigest: digest, imageTag: `0-${ECR_ISOLATED_TAG}-12345` })); + + await fixture.cdkGarbageCollect({ + rollbackBufferDays: 100, // this will ensure that we do not delete assets immediately (and just tag them) + type: 'ecr', + bootstrapStackName: toolkitStackName, + }); + fixture.log('Garbage collection complete!'); + + await fixture.aws.ecr.send(new ListImagesCommand({ repositoryName: repoName })) + .then((result) => { + expect(result.imageIds).toHaveLength(1); // the second tag has been removed + }); + + await fixture.cdkDestroy('docker-in-use', { + options: [ + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-untags-in-use-s3-objects.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-untags-in-use-s3-objects.integtest.ts new file mode 100644 index 000000000..2e055f689 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-garbage-collection-untags-in-use-s3-objects.integtest.ts @@ -0,0 +1,63 @@ +import { GetObjectTaggingCommand, ListObjectsV2Command, PutObjectTaggingCommand } from '@aws-sdk/client-s3'; +import { integTest, withoutBootstrap, randomString } from '../../lib'; + +const S3_ISOLATED_TAG = 'aws-cdk:isolated'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Garbage Collection untags in-use s3 objects', + withoutBootstrap(async (fixture) => { + const toolkitStackName = fixture.bootstrapStackName; + const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`; + fixture.rememberToDeleteBucket(bootstrapBucketName); // just in case + + await fixture.cdkBootstrapModern({ + toolkitStackName, + bootstrapBucketName, + }); + + await fixture.cdkDeploy('lambda', { + options: [ + '--context', `bootstrapBucket=${bootstrapBucketName}`, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + fixture.log('Setup complete!'); + + // Artificially add tagging to the asset in the bootstrap bucket + const result = await fixture.aws.s3.send(new ListObjectsV2Command({ Bucket: bootstrapBucketName })); + const key = result.Contents!.filter((c) => c.Key?.split('.')[1] == 'zip')[0].Key; // fancy footwork to make sure we have the asset key + await fixture.aws.s3.send(new PutObjectTaggingCommand({ + Bucket: bootstrapBucketName, + Key: key, + Tagging: { + TagSet: [{ + Key: S3_ISOLATED_TAG, + Value: '12345', + }, { + Key: 'bogus', + Value: 'val', + }], + }, + })); + + await fixture.cdkGarbageCollect({ + rollbackBufferDays: 100, // this will ensure that we do not delete assets immediately (and just tag them) + type: 's3', + bootstrapStackName: toolkitStackName, + }); + fixture.log('Garbage collection complete!'); + + // assert that the isolated object tag is removed while the other tag remains + const newTags = await fixture.aws.s3.send(new GetObjectTaggingCommand({ Bucket: bootstrapBucketName, Key: key })); + + expect(newTags.TagSet).toEqual([{ + Key: 'bogus', + Value: 'val', + }]); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-generating-and-loading-assembly.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-generating-and-loading-assembly.integtest.ts new file mode 100644 index 000000000..59b92da2e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-generating-and-loading-assembly.integtest.ts @@ -0,0 +1,52 @@ +import { promises as fs } from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'generating and loading assembly', + withDefaultFixture(async (fixture) => { + const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`; + await fixture.shell(['rm', '-rf', asmOutputDir]); + + // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory. + await fixture.cdk(['synth']); + await fixture.cdk(['synth', '--output', asmOutputDir]); + + // cdk.out in the current directory and the indicated --output should be the same + await fixture.shell(['diff', 'cdk.out', asmOutputDir]); + + // Check that we can 'ls' the synthesized asm. + // Change to some random directory to make sure we're not accidentally loading cdk.json + const list = await fixture.cdk(['--app', asmOutputDir, 'ls'], { cwd: os.tmpdir() }); + // Same stacks we know are in the app + expect(list).toContain(`${fixture.stackNamePrefix}-lambda`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-1`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-2`); + + // Check that we can use '.' and just synth ,the generated asm + const stackTemplate = await fixture.cdk(['--app', '.', 'synth', fixture.fullStackName('test-2')], { + cwd: asmOutputDir, + }); + expect(stackTemplate).toContain('topic152D84A37'); + + // Deploy a Lambda from the copied asm + await fixture.cdkDeploy('lambda', { options: ['-a', '.'], cwd: asmOutputDir }); + + // Remove (rename) the original custom docker file that was used during synth. + // this verifies that the assemly has a copy of it and that the manifest uses + // relative paths to reference to it. + const customDockerFile = path.join(fixture.integTestDir, 'docker', 'Dockerfile.Custom'); + await fs.rename(customDockerFile, `${customDockerFile}~`); + try { + // deploy a docker image with custom file without synth (uses assets) + await fixture.cdkDeploy('docker-with-custom-file', { options: ['-a', '.'], cwd: asmOutputDir }); + } finally { + // Rename back to restore fixture to original state + await fs.rename(`${customDockerFile}~`, customDockerFile); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-for-ecs-service-detects-failed-deployment-and-errors.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-for-ecs-service-detects-failed-deployment-and-errors.integtest.ts new file mode 100644 index 000000000..fda1c1d91 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-for-ecs-service-detects-failed-deployment-and-errors.integtest.ts @@ -0,0 +1,29 @@ +import { integTest, withExtendedTimeoutFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'hotswap deployment for ecs service detects failed deployment and errors', + withExtendedTimeoutFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('ecs-hotswap', { verbose: true }); + + // WHEN + const deployOutput = await fixture.cdkDeploy('ecs-hotswap', { + options: ['--hotswap'], + modEnv: { + USE_INVALID_ECS_HOTSWAP_IMAGE: 'true', + }, + allowErrExit: true, + verbose: true, + }); + + // THEN + const expectedSubstring = 'Resource is not in the expected state due to waiter status: TIMEOUT'; + expect(deployOutput).toContain(expectedSubstring); + expect(deployOutput).toContain('Observed responses:'); + expect(deployOutput).toContain('200: OK'); + expect(deployOutput).not.toContain('hotswapped!'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-for-ecs-service-waits-for-deployment-to-complete.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-for-ecs-service-waits-for-deployment-to-complete.integtest.ts new file mode 100644 index 000000000..235d054ad --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-for-ecs-service-waits-for-deployment-to-complete.integtest.ts @@ -0,0 +1,45 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { DescribeServicesCommand } from '@aws-sdk/client-ecs'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'hotswap deployment for ecs service waits for deployment to complete', + withDefaultFixture(async (fixture) => { + // GIVEN + const stackArn = await fixture.cdkDeploy('ecs-hotswap', { + captureStderr: false, + }); + + // WHEN + const deployOutput = await fixture.cdkDeploy('ecs-hotswap', { + options: ['--hotswap'], + modEnv: { + DYNAMIC_ECS_PROPERTY_VALUE: 'new value', + }, + }); + + const describeStacksResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + const clusterName = describeStacksResponse.Stacks?.[0].Outputs?.find((output) => output.OutputKey == 'ClusterName') + ?.OutputValue!; + const serviceName = describeStacksResponse.Stacks?.[0].Outputs?.find((output) => output.OutputKey == 'ServiceName') + ?.OutputValue!; + + // THEN + + const describeServicesResponse = await fixture.aws.ecs.send( + new DescribeServicesCommand({ + cluster: clusterName, + services: [serviceName], + }), + ); + expect(describeServicesResponse.services?.[0].deployments).toHaveLength(1); // only one deployment present + expect(deployOutput).toMatch(/hotswapped!/); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-appsync-apis-with-many-functions.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-appsync-apis-with-many-functions.integtest.ts new file mode 100644 index 000000000..fd7f8bbe5 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-appsync-apis-with-many-functions.integtest.ts @@ -0,0 +1,36 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('hotswap deployment supports AppSync APIs with many functions', + withDefaultFixture(async (fixture) => { + // GIVEN + const stackArn = await fixture.cdkDeploy('appsync-hotswap', { + captureStderr: false, + }); + + // WHEN + const deployOutput = await fixture.cdkDeploy('appsync-hotswap', { + options: ['--hotswap'], + captureStderr: true, + onlyStderr: true, + modEnv: { + DYNAMIC_APPSYNC_PROPERTY_VALUE: '$util.qr($ctx.stash.put("newTemplate", []))\n$util.toJson({})', + }, + }); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + + expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + // assert all 50 functions were hotswapped + for (const i of Array(50).keys()) { + expect(deployOutput).toContain(`AWS::AppSync::FunctionConfiguration 'appsync_function${i}' hotswapped!`); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-ecs-service.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-ecs-service.integtest.ts new file mode 100644 index 000000000..069dc5a40 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-ecs-service.integtest.ts @@ -0,0 +1,41 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'hotswap deployment supports ecs service', + withDefaultFixture(async (fixture) => { + // GIVEN + const stackArn = await fixture.cdkDeploy('ecs-hotswap', { + captureStderr: false, + }); + + // WHEN + const deployOutput = await fixture.cdkDeploy('ecs-hotswap', { + options: ['--hotswap'], + captureStderr: true, + onlyStderr: true, + modEnv: { + DYNAMIC_ECS_PROPERTY_VALUE: 'new value', + }, + }); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + const serviceName = response.Stacks?.[0].Outputs?.find((output) => output.OutputKey == 'ServiceName')?.OutputValue; + + // THEN + + // The deployment should not trigger a full deployment, thus the stack's status must remains + // "CREATE_COMPLETE" + expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + // The entire string fails locally due to formatting. Making this test less specific + expect(deployOutput).toMatch(/hotswapped!/); + expect(deployOutput).toContain(serviceName); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-fn::importvalue-intrinsic.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-fn::importvalue-intrinsic.integtest.ts new file mode 100644 index 000000000..67eaff16a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-fn::importvalue-intrinsic.integtest.ts @@ -0,0 +1,53 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'hotswap deployment supports Fn::ImportValue intrinsic', + withDefaultFixture(async (fixture) => { + // GIVEN + try { + await fixture.cdkDeploy('export-value-stack'); + const stackArn = await fixture.cdkDeploy('lambda-hotswap', { + captureStderr: false, + modEnv: { + DYNAMIC_LAMBDA_PROPERTY_VALUE: 'original value', + USE_IMPORT_VALUE_LAMBDA_PROPERTY: 'true', + }, + }); + + // WHEN + const deployOutput = await fixture.cdkDeploy('lambda-hotswap', { + options: ['--hotswap'], + captureStderr: true, + onlyStderr: true, + modEnv: { + DYNAMIC_LAMBDA_PROPERTY_VALUE: 'new value', + USE_IMPORT_VALUE_LAMBDA_PROPERTY: 'true', + }, + }); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + const functionName = response.Stacks?.[0].Outputs?.[0].OutputValue; + + // THEN + + // The deployment should not trigger a full deployment, thus the stack's status must remains + // "CREATE_COMPLETE" + expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + // The entire string fails locally due to formatting. Making this test less specific + expect(deployOutput).toMatch(/hotswapped!/); + expect(deployOutput).toContain(functionName); + } finally { + // Ensure cleanup in reverse order due to use of import/export + await fixture.cdkDestroy('lambda-hotswap'); + await fixture.cdkDestroy('export-value-stack'); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-lambda-functions-description-and-environment-variables.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-lambda-functions-description-and-environment-variables.integtest.ts new file mode 100644 index 000000000..48992f868 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-deployment-supports-lambda-functions-description-and-environment-variables.integtest.ts @@ -0,0 +1,43 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + "hotswap deployment supports Lambda function's description and environment variables", + withDefaultFixture(async (fixture) => { + // GIVEN + const stackArn = await fixture.cdkDeploy('lambda-hotswap', { + captureStderr: false, + modEnv: { + DYNAMIC_LAMBDA_PROPERTY_VALUE: 'original value', + }, + }); + + // WHEN + const deployOutput = await fixture.cdkDeploy('lambda-hotswap', { + options: ['--hotswap'], + captureStderr: true, + onlyStderr: true, + modEnv: { + DYNAMIC_LAMBDA_PROPERTY_VALUE: 'new value', + }, + }); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + const functionName = response.Stacks?.[0].Outputs?.[0].OutputValue; + + // THEN + // The deployment should not trigger a full deployment, thus the stack's status must remains + // "CREATE_COMPLETE" + expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + // The entire string fails locally due to formatting. Making this test less specific + expect(deployOutput).toMatch(/hotswapped!/); + expect(deployOutput).toContain(functionName); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-ecs-deployment-respects-properties-override.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-ecs-deployment-respects-properties-override.integtest.ts new file mode 100644 index 000000000..4665d917d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-hotswap-ecs-deployment-respects-properties-override.integtest.ts @@ -0,0 +1,59 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { DescribeServicesCommand } from '@aws-sdk/client-ecs'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('hotswap ECS deployment respects properties override', withDefaultFixture(async (fixture) => { + // Update the CDK context with the new ECS properties + let ecsMinimumHealthyPercent = 100; + let ecsMaximumHealthyPercent = 200; + let cdkJson = JSON.parse(await fs.readFile(path.join(fixture.integTestDir, 'cdk.json'), 'utf8')); + cdkJson = { + ...cdkJson, + hotswap: { + ecs: { + minimumHealthyPercent: ecsMinimumHealthyPercent, + maximumHealthyPercent: ecsMaximumHealthyPercent, + }, + }, + }; + + await fs.writeFile(path.join(fixture.integTestDir, 'cdk.json'), JSON.stringify(cdkJson)); + + // GIVEN + const stackArn = await fixture.cdkDeploy('ecs-hotswap', { + captureStderr: false, + }); + + // WHEN + await fixture.cdkDeploy('ecs-hotswap', { + options: [ + '--hotswap', + ], + modEnv: { + DYNAMIC_ECS_PROPERTY_VALUE: 'new value', + }, + }); + + const describeStacksResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + + const clusterName = describeStacksResponse.Stacks?.[0].Outputs?.find(output => output.OutputKey == 'ClusterName')?.OutputValue!; + const serviceName = describeStacksResponse.Stacks?.[0].Outputs?.find(output => output.OutputKey == 'ServiceName')?.OutputValue!; + + // THEN + const describeServicesResponse = await fixture.aws.ecs.send( + new DescribeServicesCommand({ + cluster: clusterName, + services: [serviceName], + }), + ); + expect(describeServicesResponse.services?.[0].deploymentConfiguration?.minimumHealthyPercent).toEqual(ecsMinimumHealthyPercent); + expect(describeServicesResponse.services?.[0].deploymentConfiguration?.maximumPercent).toEqual(ecsMaximumHealthyPercent); +})); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-iam-diff.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-iam-diff.integtest.ts new file mode 100644 index 000000000..5c32a46e6 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-iam-diff.integtest.ts @@ -0,0 +1,23 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'IAM diff', + withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]); + + // Roughly check for a table like this: + // + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ + // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ + + expect(output).toContain('${SomeRole.Arn}'); + expect(output).toContain('sts:AssumeRole'); + expect(output).toContain('ec2.amazonaws.com'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-import-interactive.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-import-interactive.integtest.ts new file mode 100644 index 000000000..cf0bdb5cf --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-import-interactive.integtest.ts @@ -0,0 +1,53 @@ +import { DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation'; +import { CreateTopicCommand, DeleteTopicCommand } from '@aws-sdk/client-sns'; +import { integTest, withDefaultFixture } from '../../lib'; + +integTest('cdk import prompts the user for sns topic arns', withDefaultFixture(async (fixture) => { + const topicName = (logicalId: string) => `${logicalId}-${fixture.randomString}`; + const topicArn = async (name: string) => `arn:aws:sns:${fixture.aws.region}:${ await fixture.aws.account()}:${name}`; + + const topic1Name = topicName('Topic1'); + const topic2Name = topicName('Topic2'); + + const topic1Arn = await topicArn(topic1Name); + const topic2Arn = await topicArn(topic2Name); + + fixture.log(`Creating topic ${topic1Name}`); + await fixture.aws.sns.send(new CreateTopicCommand({ Name: topic1Name })); + fixture.log(`Creating topic ${topic2Name}`); + await fixture.aws.sns.send(new CreateTopicCommand({ Name: topic2Name })); + + try { + const stackName = 'two-sns-topics'; + const fullStackName = fixture.fullStackName(stackName); + + fixture.log(`Importing topics to stack ${fullStackName}`); + await fixture.cdk(['import', fullStackName], { + interact: [ + { + prompt: /Topic1.*\(empty to skip\):/, + input: topic1Arn, + }, + { + prompt: /Topic2.*\(empty to skip\):/, + input: topic2Arn, + }, + ], + modEnv: { + // disable coloring because it messes up prompt matching. + FORCE_COLOR: '0', + }, + }); + + // assert the stack now has the two topics + const stackResources = await fixture.aws.cloudFormation.send(new DescribeStackResourcesCommand({ StackName: fullStackName })); + const stackTopicArns = new Set(stackResources.StackResources?.filter(r => r.ResourceType === 'AWS::SNS::Topic').map(r => r.PhysicalResourceId) ?? []); + + expect(stackTopicArns).toEqual(new Set([topic1Arn, topic2Arn])); + } finally { + fixture.log(`Deleting topic ${topic1Name}`); + await fixture.aws.sns.send(new DeleteTopicCommand({ TopicArn: topic1Arn })); + fixture.log(`Deleting topic ${topic2Name}`); + await fixture.aws.sns.send(new DeleteTopicCommand({ TopicArn: topic2Arn })); + } +})); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-cli-lib-deploy.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-cli-lib-deploy.integtest.ts new file mode 100644 index 000000000..6c03e19b6 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-cli-lib-deploy.integtest.ts @@ -0,0 +1,32 @@ +import { DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withCliLibFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cli-lib deploy', + withCliLibFixture(async (fixture) => { + const stackName = fixture.fullStackName('simple-1'); + + try { + // deploy the stack + await fixture.cdk(['deploy', stackName], { + neverRequireApproval: true, + }); + + // verify the number of resources in the stack + const expectedStack = await fixture.aws.cloudFormation.send( + new DescribeStackResourcesCommand({ + StackName: stackName, + }), + ); + expect(expectedStack.StackResources?.length).toEqual(3); + } finally { + // delete the stack + await fixture.cdk(['destroy', stackName], { + captureStderr: false, + }); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-cli-lib-list.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-cli-lib-list.integtest.ts new file mode 100644 index 000000000..1b7a12d37 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-cli-lib-list.integtest.ts @@ -0,0 +1,12 @@ +import { integTest, withCliLibFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cli-lib list', + withCliLibFixture(async (fixture) => { + const listing = await fixture.cdk(['list'], { captureStderr: false }); + expect(listing).toContain(fixture.fullStackName('simple-1')); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-cli-lib-synth.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-cli-lib-synth.integtest.ts new file mode 100644 index 000000000..5acb70a4f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-cli-lib-synth.integtest.ts @@ -0,0 +1,27 @@ +import { integTest, withCliLibFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cli-lib synth', + withCliLibFixture(async (fixture) => { + await fixture.cdk(['synth', fixture.fullStackName('simple-1')]); + expect(fixture.template('simple-1')).toEqual( + expect.objectContaining({ + // Checking for a small subset is enough as proof that synth worked + Resources: expect.objectContaining({ + queue276F7297: expect.objectContaining({ + Type: 'AWS::SQS::Queue', + Properties: { + VisibilityTimeout: 300, + }, + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-simple-1/queue/Resource`, + }, + }), + }), + }), + ); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-security-related-changes-without-a-cli-are-expected-to-fail-when-approval-is-required.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-security-related-changes-without-a-cli-are-expected-to-fail-when-approval-is-required.integtest.ts new file mode 100644 index 000000000..63d9706ec --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-security-related-changes-without-a-cli-are-expected-to-fail-when-approval-is-required.integtest.ts @@ -0,0 +1,32 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withCliLibFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'security related changes without a CLI are expected to fail when approval is required', + withCliLibFixture(async (fixture) => { + const stdErr = await fixture.cdk(['deploy', fixture.fullStackName('simple-1')], { + onlyStderr: true, + captureStderr: true, + allowErrExit: true, + neverRequireApproval: false, + }); + + expect(stdErr).toContain( + 'This deployment will make potentially sensitive changes according to your current security approval level', + ); + expect(stdErr).toContain( + '"--require-approval" is enabled and stack includes security-sensitive updates', + ); + + // Ensure stack was not deployed + await expect( + fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: fixture.fullStackName('simple-1'), + }), + ), + ).rejects.toThrow('does not exist'); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-csharp.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-csharp.integtest.ts new file mode 100644 index 000000000..bab29cd61 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-csharp.integtest.ts @@ -0,0 +1,13 @@ +import { fromStackCreatesDeployableApp } from './testcase'; +import { integTest, withExtendedTimeoutFixture } from '../../../lib'; + +const language = 'csharp'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + `cdk migrate --from-stack creates deployable ${language} app`, + withExtendedTimeoutFixture(async (fixture) => { + await fromStackCreatesDeployableApp(fixture, language); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-java.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-java.integtest.ts new file mode 100644 index 000000000..6b227c337 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-java.integtest.ts @@ -0,0 +1,13 @@ +import { fromStackCreatesDeployableApp } from './testcase'; +import { integTest, withExtendedTimeoutFixture } from '../../../lib'; + +const language = 'java'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + `cdk migrate --from-stack creates deployable ${language} app`, + withExtendedTimeoutFixture(async (fixture) => { + await fromStackCreatesDeployableApp(fixture, language); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-python.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-python.integtest.ts new file mode 100644 index 000000000..f1a76e029 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-python.integtest.ts @@ -0,0 +1,13 @@ +import { fromStackCreatesDeployableApp } from './testcase'; +import { integTest, withExtendedTimeoutFixture } from '../../../lib'; + +const language = 'python'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + `cdk migrate --from-stack creates deployable ${language} app`, + withExtendedTimeoutFixture(async (fixture) => { + await fromStackCreatesDeployableApp(fixture, language); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-typescript.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-typescript.integtest.ts new file mode 100644 index 000000000..632330336 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate--from-stack-creates-deployable-app-typescript.integtest.ts @@ -0,0 +1,13 @@ +import { fromStackCreatesDeployableApp } from './testcase'; +import { integTest, withExtendedTimeoutFixture } from '../../../lib'; + +const language = 'typescript'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + `cdk migrate --from-stack creates deployable ${language} app`, + withExtendedTimeoutFixture(async (fixture) => { + await fromStackCreatesDeployableApp(fixture, language); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-csharp.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-csharp.integtest.ts new file mode 100644 index 000000000..6c4be0054 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-csharp.integtest.ts @@ -0,0 +1,13 @@ +import { deploysSuccessfully } from './testcase'; +import { integTest, withCDKMigrateFixture } from '../../../lib'; + +const language = 'csharp'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + `cdk migrate ${language} deploys successfully`, + withCDKMigrateFixture(language, async (fixture) => { + await deploysSuccessfully(fixture, language); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-java.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-java.integtest.ts new file mode 100644 index 000000000..258f8f970 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-java.integtest.ts @@ -0,0 +1,13 @@ +import { deploysSuccessfully } from './testcase'; +import { integTest, withCDKMigrateFixture } from '../../../lib'; + +const language = 'java'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + `cdk migrate ${language} deploys successfully`, + withCDKMigrateFixture(language, async (fixture) => { + await deploysSuccessfully(fixture, language); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-python.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-python.integtest.ts new file mode 100644 index 000000000..d5bdcbdbd --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-python.integtest.ts @@ -0,0 +1,13 @@ +import { deploysSuccessfully } from './testcase'; +import { integTest, withCDKMigrateFixture } from '../../../lib'; + +const language = 'python'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + `cdk migrate ${language} deploys successfully`, + withCDKMigrateFixture(language, async (fixture) => { + await deploysSuccessfully(fixture, language); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-typescript.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-typescript.integtest.ts new file mode 100644 index 000000000..11ad8c8d8 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-deploys-successfully-typescript.integtest.ts @@ -0,0 +1,13 @@ +import { deploysSuccessfully } from './testcase'; +import { integTest, withCDKMigrateFixture } from '../../../lib'; + +const language = 'typescript'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + `cdk migrate ${language} deploys successfully`, + withCDKMigrateFixture(language, async (fixture) => { + await deploysSuccessfully(fixture, language); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-generates-migrate.json.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-generates-migrate.json.integtest.ts new file mode 100644 index 000000000..907cfdbd5 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/cdk-migrate-generates-migrate.json.integtest.ts @@ -0,0 +1,19 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { integTest, withCDKMigrateFixture } from '../../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk migrate generates migrate.json', + withCDKMigrateFixture('typescript', async (fixture) => { + const migrateFile = await fs.readFile(path.join(fixture.integTestDir, 'migrate.json'), 'utf8'); + const expectedFile = `{ + \"//\": \"This file is generated by cdk migrate. It will be automatically deleted after the first successful deployment of this app to the environment of the original resources.\", + \"Source\": \"localfile\" + }`; + expect(JSON.parse(migrateFile)).toEqual(JSON.parse(expectedFile)); + await fixture.cdkDestroy(fixture.stackNamePrefix); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/testcase.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/testcase.ts new file mode 100644 index 000000000..3701bdf94 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-migrate/testcase.ts @@ -0,0 +1,68 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { CreateStackCommand, DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import type { TestFixture } from '../../../lib'; +import { sleep } from '../../../lib'; + +export async function deploysSuccessfully(fixture: TestFixture, language: string) { + if (language === 'python') { + await fixture.shell(['pip', 'install', '-r', 'requirements.txt']); + } + + const stackArn = await fixture.cdkDeploy( + fixture.stackNamePrefix, + { neverRequireApproval: true, verbose: true, captureStderr: false }, + true, + ); + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + + expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + await fixture.cdkDestroy(fixture.stackNamePrefix); +} + +export async function fromStackCreatesDeployableApp(fixture: TestFixture, language: string) { + const migrateStackName = fixture.fullStackName('migrate-stack'); + await fixture.aws.cloudFormation.send( + new CreateStackCommand({ + StackName: migrateStackName, + TemplateBody: await fs.readFile( + path.join(__dirname, '..', '..', '..', 'resources', 'templates', 'sqs-template.json'), + 'utf8', + ), + }), + ); + try { + let stackStatus = 'CREATE_IN_PROGRESS'; + while (stackStatus === 'CREATE_IN_PROGRESS') { + stackStatus = await ( + await fixture.aws.cloudFormation.send(new DescribeStacksCommand({ StackName: migrateStackName })) + ).Stacks?.[0].StackStatus!; + await sleep(1000); + } + await fixture.cdk(['migrate', '--language', language, '--stack-name', migrateStackName, '--from-stack'], { + modEnv: { MIGRATE_INTEG_TEST: '1' }, + neverRequireApproval: true, + verbose: true, + captureStderr: false, + }); + await fixture.shell(['cd', path.join(fixture.integTestDir, migrateStackName)]); + await fixture.cdk(['deploy', migrateStackName], { + neverRequireApproval: true, + verbose: true, + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: migrateStackName, + }), + ); + + expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_COMPLETE'); + } finally { + await fixture.cdkDestroy('migrate-stack'); + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-nested-stack-with-parameters.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-nested-stack-with-parameters.integtest.ts new file mode 100644 index 000000000..b7ae5242b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-nested-stack-with-parameters.integtest.ts @@ -0,0 +1,28 @@ +import { DescribeStackResourcesCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'nested stack with parameters', + withDefaultFixture(async (fixture) => { + // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances + // of this test to run in parallel, othewise they will attempt to create the same SNS topic. + const stackArn = await fixture.cdkDeploy('with-nested-stack-using-parameters', { + options: ['--parameters', `MyTopicParam=${fixture.stackNamePrefix}ThereIsNoSpoon`], + captureStderr: false, + }); + + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation.send( + new DescribeStackResourcesCommand({ + StackName: stackArn, + }), + ); + expect(response.StackResources?.length).toEqual(1); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-requests-go-through-a-proxy-when-configured.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-requests-go-through-a-proxy-when-configured.integtest.ts new file mode 100644 index 000000000..4c005cef3 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-requests-go-through-a-proxy-when-configured.integtest.ts @@ -0,0 +1,39 @@ +import { promises as fs } from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../lib'; +import { awsActionsFromRequests, startProxyServer } from '../../lib/proxy'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest('requests go through a proxy when configured', + withDefaultFixture(async (fixture) => { + const proxyServer = await startProxyServer(); + try { + // Delete notices cache if it exists + await fs.rm(path.join(process.env.HOME ?? os.userInfo().homedir, '.cdk/cache/notices.json'), { force: true }); + + await fixture.cdkDeploy('test-2', { + captureStderr: true, + options: [ + '--proxy', proxyServer.url, + '--ca-bundle-path', proxyServer.certPath, + ], + modEnv: { + CDK_HOME: fixture.integTestDir, + }, + }); + + const requests = await proxyServer.getSeenRequests(); + + expect(requests.map(req => req.url)) + .toContain('https://cli.cdk.dev-tools.aws.dev/notices.json'); + + const actionsUsed = awsActionsFromRequests(requests); + expect(actionsUsed).toContain('AssumeRole'); + expect(actionsUsed).toContain('CreateChangeSet'); + } finally { + await proxyServer.stop(); + } + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-sam-can-locally-test-the-synthesized-cdk-application.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-sam-can-locally-test-the-synthesized-cdk-application.integtest.ts new file mode 100644 index 000000000..197642da1 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-sam-can-locally-test-the-synthesized-cdk-application.integtest.ts @@ -0,0 +1,25 @@ +import { integTest, withSamIntegrationFixture, randomInteger } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'sam can locally test the synthesized cdk application', + withSamIntegrationFixture(async (fixture) => { + // Synth first + await fixture.cdkSynth(); + + const result = await fixture.samLocalStartApi( + 'TestStack', + false, + randomInteger(30000, 40000), + '/restapis/spec/pythonFunction', + ); + expect(result.actionSucceeded).toBeTruthy(); + expect(result.actionOutput).toEqual( + expect.objectContaining({ + message: 'Hello World', + }), + ); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-security-related-changes-without-a-cli-are-expected-to-fail.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-security-related-changes-without-a-cli-are-expected-to-fail.integtest.ts new file mode 100644 index 000000000..28811c3c6 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-security-related-changes-without-a-cli-are-expected-to-fail.integtest.ts @@ -0,0 +1,30 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'security related changes without a CLI are expected to fail', + withDefaultFixture(async (fixture) => { + // redirect /dev/null to stdin, which means there will not be tty attached + // since this stack includes security-related changes, the deployment should + // immediately fail because we can't confirm the changes + const stackName = 'iam-test'; + await expect( + fixture.cdkDeploy(stackName, { + options: ['<', '/dev/null'], // H4x, this only works because I happen to know we pass shell: true. + neverRequireApproval: false, + }), + ).rejects.toThrow('exited with error'); + + // Ensure stack was not deployed + await expect( + fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: fixture.fullStackName(stackName), + }), + ), + ).rejects.toThrow('does not exist'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-skips-notice-refresh.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-skips-notice-refresh.integtest.ts new file mode 100644 index 000000000..13e80eddd --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-skips-notice-refresh.integtest.ts @@ -0,0 +1,26 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'skips notice refresh', + withDefaultFixture(async (fixture) => { + const output = await fixture.cdkSynth({ + options: ['--no-notices'], + modEnv: { + INTEG_STACK_SET: 'stage-using-context', + }, + allowErrExit: true, + }); + + // Neither succeeds nor fails, but skips the refresh + await expect(output).not.toContain('Notices refreshed'); + await expect(output).not.toContain('Notices refresh failed'); + }), +); + +/** + * Create an S3 bucket, orphan that bucket, then import the bucket, with a NodeJSFunction lambda also in the stack. + * + * Validates fix for https://github.com/aws/aws-cdk/issues/31999 (import fails) + */ diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-ssm-parameter-provider-error.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-ssm-parameter-provider-error.integtest.ts new file mode 100644 index 000000000..083dd26fa --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-ssm-parameter-provider-error.integtest.ts @@ -0,0 +1,18 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'ssm parameter provider error', + withDefaultFixture(async (fixture) => { + await expect( + fixture.cdk( + ['synth', fixture.fullStackName('missing-ssm-parameter'), '-c', 'test:ssm-parameter-name=/does/not/exist'], + { + allowErrExit: true, + }, + ), + ).resolves.toContain('SSM parameter not available in account'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-stack-in-update_rollback_complete-state-can-be-updated.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-stack-in-update_rollback_complete-state-can-be-updated.integtest.ts new file mode 100644 index 000000000..e715bb476 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-stack-in-update_rollback_complete-state-can-be-updated.integtest.ts @@ -0,0 +1,59 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'stack in UPDATE_ROLLBACK_COMPLETE state can be updated', + withDefaultFixture(async (fixture) => { + // GIVEN + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}nice`], + captureStderr: false, + }); + + let response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + + expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + + // bad parameter name with @ will put stack into UPDATE_ROLLBACK_COMPLETE + await expect( + fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`], + captureStderr: false, + }), + ).rejects.toThrow('exited with error'); + + response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + + expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_ROLLBACK_COMPLETE'); + + // WHEN + await fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`], + captureStderr: false, + }); + + response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: stackArn, + }), + ); + + // THEN + expect(response.Stacks?.[0].StackStatus).toEqual('UPDATE_COMPLETE'); + expect(response.Stacks?.[0].Parameters).toContainEqual({ + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-stage-with-bundled-lambda-function.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-stage-with-bundled-lambda-function.integtest.ts new file mode 100644 index 000000000..a496538d4 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-stage-with-bundled-lambda-function.integtest.ts @@ -0,0 +1,13 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Stage with bundled Lambda function', + withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('bundling-stage/BundlingStack'); + fixture.log('Setup complete!'); + await fixture.cdkDestroy('bundling-stage/BundlingStack'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-synth---quiet-can-be-specified-in-cdk.json.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-synth---quiet-can-be-specified-in-cdk.json.integtest.ts new file mode 100644 index 000000000..3bdedf0a3 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-synth---quiet-can-be-specified-in-cdk.json.integtest.ts @@ -0,0 +1,20 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'synth --quiet can be specified in cdk.json', + withDefaultFixture(async (fixture) => { + let cdkJson = JSON.parse(await fs.readFile(path.join(fixture.integTestDir, 'cdk.json'), 'utf8')); + cdkJson = { + ...cdkJson, + quiet: true, + }; + await fs.writeFile(path.join(fixture.integTestDir, 'cdk.json'), JSON.stringify(cdkJson)); + const synthOutput = await fixture.cdk(['synth', fixture.fullStackName('test-2')]); + expect(synthOutput).not.toContain('topic152D84A37'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-synthing-a-stage-with-errors-can-be-suppressed.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-synthing-a-stage-with-errors-can-be-suppressed.integtest.ts new file mode 100644 index 000000000..2d3e00d9d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-synthing-a-stage-with-errors-can-be-suppressed.integtest.ts @@ -0,0 +1,15 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'synthing a stage with errors can be suppressed', + withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', '--no-validation'], { + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', + }, + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-synthing-a-stage-with-errors-leads-to-failure.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-synthing-a-stage-with-errors-leads-to-failure.integtest.ts new file mode 100644 index 000000000..990e65b99 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-synthing-a-stage-with-errors-leads-to-failure.integtest.ts @@ -0,0 +1,18 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'synthing a stage with errors leads to failure', + withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['synth'], { + allowErrExit: true, + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', + }, + }); + + expect(output).toContain('This is an error'); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-templates-on-disk-contain-metadata-resource.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-templates-on-disk-contain-metadata-resource.integtest.ts new file mode 100644 index 000000000..9f37cdf9b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-templates-on-disk-contain-metadata-resource.integtest.ts @@ -0,0 +1,25 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'templates on disk contain metadata resource, also in nested assemblies', + withDefaultFixture(async (fixture) => { + // Synth first, and switch on version reporting because cdk.json is disabling it + await fixture.cdk(['synth', '--version-reporting=true']); + + // Load template from disk from root assembly + const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']); + + expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); + + // Load template from nested assembly + const nestedTemplateContents = await fixture.shell([ + 'cat', + 'cdk.out/assembly-*-stage/*StackInStage*.template.json', + ]); + + expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-termination-protection.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-termination-protection.integtest.ts new file mode 100644 index 000000000..18fcb887e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-termination-protection.integtest.ts @@ -0,0 +1,19 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Termination protection', + withDefaultFixture(async (fixture) => { + const stackName = 'termination-protection'; + await fixture.cdkDeploy(stackName); + + // Try a destroy that should fail + await expect(fixture.cdkDestroy(stackName)).rejects.toThrow('exited with error'); + + // Can update termination protection even though the change set doesn't contain changes + await fixture.cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); + await fixture.cdkDestroy(stackName); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-cdk-rollback---force.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-cdk-rollback---force.integtest.ts new file mode 100644 index 000000000..b38e06245 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-cdk-rollback---force.integtest.ts @@ -0,0 +1,48 @@ +import { integTest, withSpecificFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'test cdk rollback --force', + withSpecificFixture('rollback-test-app', async (fixture) => { + let phase = '1'; + + // Should succeed + await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + }); + try { + phase = '2b'; // Fail update and also fail rollback + + // Should fail + const deployOutput = await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + allowErrExit: true, + }); + + expect(deployOutput).toContain('UPDATE_FAILED'); + + // Should still fail + const rollbackOutput = await fixture.cdk(['rollback'], { + modEnv: { PHASE: phase }, + verbose: false, + allowErrExit: true, + }); + + expect(rollbackOutput).toContain('Failing rollback'); + + // Rollback and force cleanup + await fixture.cdk(['rollback', '--force'], { + modEnv: { PHASE: phase }, + verbose: false, + }); + } finally { + await fixture.cdkDestroy('test-rollback'); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-cdk-rollback.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-cdk-rollback.integtest.ts new file mode 100644 index 000000000..c0ef0c264 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-cdk-rollback.integtest.ts @@ -0,0 +1,38 @@ +import { integTest, withSpecificFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'test cdk rollback', + withSpecificFixture('rollback-test-app', async (fixture) => { + let phase = '1'; + + // Should succeed + await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + }); + try { + phase = '2a'; + + // Should fail + const deployOutput = await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + allowErrExit: true, + }); + expect(deployOutput).toContain('UPDATE_FAILED'); + + // Rollback + await fixture.cdk(['rollback'], { + modEnv: { PHASE: phase }, + verbose: false, + }); + } finally { + await fixture.cdkDestroy('test-rollback'); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-migrate-deployment-for-app-with-localfile-source-in-migrate.json.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-migrate-deployment-for-app-with-localfile-source-in-migrate.json.integtest.ts new file mode 100644 index 000000000..ea241cba9 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-migrate-deployment-for-app-with-localfile-source-in-migrate.json.integtest.ts @@ -0,0 +1,56 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'test migrate deployment for app with localfile source in migrate.json', + withDefaultFixture(async (fixture) => { + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs.mkdir(path.dirname(outputsFile), { recursive: true }); + + // Initial deploy + await fixture.cdkDeploy('migrate-stack', { + modEnv: { ORPHAN_TOPIC: '1' }, + options: ['--outputs-file', outputsFile], + }); + + const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + const stackName = fixture.fullStackName('migrate-stack'); + const queueName = outputs[stackName].QueueName; + const queueUrl = outputs[stackName].QueueUrl; + const queueLogicalId = outputs[stackName].QueueLogicalId; + fixture.log(`Created queue ${queueUrl} in stack ${stackName}`); + + // Write the migrate file based on the ID from step one, then deploy the app with migrate + const migrateFile = path.join(fixture.integTestDir, 'migrate.json'); + await fs.writeFile( + migrateFile, + JSON.stringify({ + Source: 'localfile', + Resources: [ + { + ResourceType: 'AWS::SQS::Queue', + LogicalResourceId: queueLogicalId, + ResourceIdentifier: { QueueUrl: queueUrl }, + }, + ], + }), + { encoding: 'utf-8' }, + ); + + await fixture.cdkDestroy('migrate-stack'); + fixture.log(`Deleted stack ${stackName}, orphaning ${queueName}`); + + // Create new stack from existing queue + try { + fixture.log(`Deploying new stack ${stackName}, migrating ${queueName} into stack`); + await fixture.cdkDeploy('migrate-stack'); + } finally { + // Cleanup + await fixture.cdkDestroy('migrate-stack'); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-resource-import-with-construct-that-requires-bundling.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-resource-import-with-construct-that-requires-bundling.integtest.ts new file mode 100644 index 000000000..f20e6952e --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-resource-import-with-construct-that-requires-bundling.integtest.ts @@ -0,0 +1,74 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { DescribeStacksCommand, GetTemplateCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'test resource import with construct that requires bundling', + withDefaultFixture(async (fixture) => { + // GIVEN + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs.mkdir(path.dirname(outputsFile), { recursive: true }); + + // First, create a stack that includes a NodeJSFunction lambda and one bucket that will be removed from the stack but NOT deleted from AWS. + await fixture.cdkDeploy('importable-stack', { + modEnv: { INCLUDE_NODEJS_FUNCTION_LAMBDA: '1', INCLUDE_SINGLE_BUCKET: '1', RETAIN_SINGLE_BUCKET: '1' }, + options: ['--outputs-file', outputsFile], + }); + + try { + // Second, now the bucket we will remove is in the stack and has a logicalId. We can now make the resource mapping file. + // This resource mapping file will be used to tell the import operation what bucket to bring into the stack. + const fullStackName = fixture.fullStackName('importable-stack'); + const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + const bucketLogicalId = outputs[fullStackName].BucketLogicalId; + const bucketName = outputs[fullStackName].BucketName; + const bucketResourceMap = { + [bucketLogicalId]: { + BucketName: bucketName, + }, + }; + const mappingFile = path.join(fixture.integTestDir, 'outputs', 'mapping.json'); + await fs.writeFile(mappingFile, JSON.stringify(bucketResourceMap), { encoding: 'utf-8' }); + + // Third, remove the bucket from the stack, but don't delete the bucket from AWS. + await fixture.cdkDeploy('importable-stack', { + modEnv: { INCLUDE_NODEJS_FUNCTION_LAMBDA: '1', INCLUDE_SINGLE_BUCKET: '0', RETAIN_SINGLE_BUCKET: '0' }, + }); + const cfnTemplateBeforeImport = await fixture.aws.cloudFormation.send( + new GetTemplateCommand({ StackName: fullStackName }), + ); + expect(cfnTemplateBeforeImport.TemplateBody).not.toContain(bucketLogicalId); + + // WHEN + await fixture.cdk(['import', '--resource-mapping', mappingFile, fixture.fullStackName('importable-stack')], { + modEnv: { INCLUDE_NODEJS_FUNCTION_LAMBDA: '1', INCLUDE_SINGLE_BUCKET: '1', RETAIN_SINGLE_BUCKET: '0' }, + }); + + // THEN + const describeStacksResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ StackName: fullStackName }), + ); + const cfnTemplateAfterImport = await fixture.aws.cloudFormation.send( + new GetTemplateCommand({ StackName: fullStackName }), + ); + + // If bundling is skipped during import for NodeJSFunction lambda, then the operation should fail and exit + expect(describeStacksResponse.Stacks![0].StackStatus).toEqual('IMPORT_COMPLETE'); + + // If the import operation is successful, the template should contain the imported bucket + expect(cfnTemplateAfterImport.TemplateBody).toContain(bucketLogicalId); + } finally { + // Clean up the resources we created + await fixture.cdkDestroy('importable-stack'); + } + }), +); + +/** + * Create a queue, orphan that queue, then import the queue. + * + * We want to test with a large template to make sure large templates can work with import. + */ diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-resource-import.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-resource-import.integtest.ts new file mode 100644 index 000000000..97e1ad735 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-test-resource-import.integtest.ts @@ -0,0 +1,64 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { DescribeStacksCommand, GetTemplateCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture, randomString } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'test resource import', + withDefaultFixture(async (fixture) => { + // GIVEN + const randomPrefix = randomString(); + const uniqueOutputsFileName = `${randomPrefix}Outputs.json`; // other tests use the outputs file. Make sure we don't collide. + const outputsFile = path.join(fixture.integTestDir, 'outputs', uniqueOutputsFileName); + await fs.mkdir(path.dirname(outputsFile), { recursive: true }); + + // First, create a stack that includes many queues, and one queue that will be removed from the stack but NOT deleted from AWS. + await fixture.cdkDeploy('importable-stack', { + modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '1', RETAIN_SINGLE_QUEUE: '1' }, + options: ['--outputs-file', outputsFile], + }); + + try { + // Second, now the queue we will remove is in the stack and has a logicalId. We can now make the resource mapping file. + // This resource mapping file will be used to tell the import operation what queue to bring into the stack. + const fullStackName = fixture.fullStackName('importable-stack'); + const outputs = JSON.parse((await fs.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + const queueLogicalId = outputs[fullStackName].QueueLogicalId; + const queueResourceMap = { + [queueLogicalId]: { QueueUrl: outputs[fullStackName].QueueUrl }, + }; + const mappingFile = path.join(fixture.integTestDir, 'outputs', `${randomPrefix}Mapping.json`); + await fs.writeFile(mappingFile, JSON.stringify(queueResourceMap), { encoding: 'utf-8' }); + + // Third, remove the queue from the stack, but don't delete the queue from AWS. + await fixture.cdkDeploy('importable-stack', { + modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '0', RETAIN_SINGLE_QUEUE: '0' }, + }); + const cfnTemplateBeforeImport = await fixture.aws.cloudFormation.send( + new GetTemplateCommand({ StackName: fullStackName }), + ); + expect(cfnTemplateBeforeImport.TemplateBody).not.toContain(queueLogicalId); + + // WHEN + await fixture.cdk(['import', '--resource-mapping', mappingFile, fixture.fullStackName('importable-stack')], { + modEnv: { LARGE_TEMPLATE: '1', INCLUDE_SINGLE_QUEUE: '1', RETAIN_SINGLE_QUEUE: '0' }, + }); + + // THEN + const describeStacksResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ StackName: fullStackName }), + ); + const cfnTemplateAfterImport = await fixture.aws.cloudFormation.send( + new GetTemplateCommand({ StackName: fullStackName }), + ); + expect(describeStacksResponse.Stacks![0].StackStatus).toEqual('IMPORT_COMPLETE'); + expect(cfnTemplateAfterImport.TemplateBody).toContain(queueLogicalId); + } finally { + // Clean up + await fixture.cdkDestroy('importable-stack'); + } + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-two-ways-of-showing-the-version.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-two-ways-of-showing-the-version.integtest.ts new file mode 100644 index 000000000..d604c1e60 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-two-ways-of-showing-the-version.integtest.ts @@ -0,0 +1,14 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'Two ways of showing the version', + withDefaultFixture(async (fixture) => { + const version1 = await fixture.cdk(['version'], { verbose: false }); + const version2 = await fixture.cdk(['--version'], { verbose: false }); + + expect(version1).toEqual(version2); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-update-to-stack-in-rollback_complete-state-will-delete-stack-and-create-a-new-one.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-update-to-stack-in-rollback_complete-state-will-delete-stack-and-create-a-new-one.integtest.ts new file mode 100644 index 000000000..d1e7fca6b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-update-to-stack-in-rollback_complete-state-will-delete-stack-and-create-a-new-one.integtest.ts @@ -0,0 +1,47 @@ +import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation'; +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one', + withDefaultFixture(async (fixture) => { + // GIVEN + await expect( + fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`], + captureStderr: false, + }), + ).rejects.toThrow('exited with error'); + + const response = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: fixture.fullStackName('param-test-1'), + }), + ); + + const stackArn = response.Stacks?.[0].StackId; + expect(response.Stacks?.[0].StackStatus).toEqual('ROLLBACK_COMPLETE'); + + // WHEN + const newStackArn = await fixture.cdkDeploy('param-test-1', { + options: ['--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`], + captureStderr: false, + }); + + const newStackResponse = await fixture.aws.cloudFormation.send( + new DescribeStacksCommand({ + StackName: newStackArn, + }), + ); + + // THEN + expect(stackArn).not.toEqual(newStackArn); // new stack was created + expect(newStackResponse.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE'); + expect(newStackResponse.Stacks?.[0].Parameters).toContainEqual({ + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }); + }), +); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-vpc-lookup.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-vpc-lookup.integtest.ts new file mode 100644 index 000000000..176647d94 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-vpc-lookup.integtest.ts @@ -0,0 +1,26 @@ +import { integTest, withDefaultFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'VPC Lookup', + withDefaultFixture(async (fixture) => { + fixture.log('Making sure we are clean before starting.'); + await fixture.cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + + fixture.log('Setting up: creating a VPC with known tags'); + await fixture.cdkDeploy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setup complete!'); + + fixture.log('Verifying we can now import that VPC'); + await fixture.cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' } }); + }), +); + +// testing a construct with a builtin Nodejs Lambda Function. +// In this case we are testing the s3.Bucket construct with the +// autoDeleteObjects prop set to true, which creates a Lambda backed +// CustomResource. Since the compiled Lambda code (e.g. __entrypoint__.js) +// is bundled as part of the CDK package, we want to make sure we don't +// introduce changes to the compiled code that could prevent the Lambda from +// executing. If we do, this test will timeout and fail. diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/proxy.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/proxy.integtest.ts new file mode 100644 index 000000000..69c3d6703 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/proxy.integtest.ts @@ -0,0 +1,169 @@ +import { promises as fs } from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { integTest } from '../../lib/integ-test'; +import { startProxyServer } from '../../lib/proxy'; +import type { TestFixture } from '../../lib/with-cdk-app'; +import { withDefaultFixture } from '../../lib/with-cdk-app'; + +const docker = process.env.CDK_DOCKER ?? 'docker'; + +integTest( + 'all calls from isolated container go through proxy', + withDefaultFixture(async (fixture) => { + // Find the 'cdk' command and make sure it is mounted into the container + const cdkFullpath = (await fixture.shell(['which', 'cdk'])).trim(); + let cdkTop = findMountableParent(cdkFullpath); + + // Run a 'cdk deploy' inside the container + const commands = [ + `env ${renderEnv(fixture.cdkShellEnv())} ${cdkFullpath} ${fixture.cdkDeployCommandLine('test-2').join(' ')} -v`, + ]; + + await runInIsolatedContainer(fixture, [cdkTop], commands); + }), +); + +async function runInIsolatedContainer(fixture: TestFixture, pathsToMount: string[], testCommands: string[]) { + pathsToMount.push( + `${process.env.HOME}`, + fixture.integTestDir, + ); + + // Resolve credential provider to an access key that we can pass into the container + const credentials = await fixture.aws.credentials(); + + const proxy = await startProxyServer(fixture.integTestDir); + try { + const proxyPort = proxy.port; + + const setupCommands = [ + 'apt-get update -qq', + 'apt-get install -qqy nodejs > /dev/null', + ...isolatedDockerCommands(proxyPort, proxy.certPath), + ]; + + const scriptName = path.join(fixture.integTestDir, 'script.sh'); + + // Write a script file + await fs.writeFile(scriptName, [ + '#!/bin/bash', + 'set -x', + 'set -eu', + ...setupCommands, + ...testCommands, + ].join('\n'), 'utf-8'); + + await fs.chmod(scriptName, 0o755); + + await fixture.ecrPublicLogin(); + + // Run commands in a Docker shell + await fixture.shell([ + docker, 'run', '--net=bridge', '--rm', + ...pathsToMount.flatMap(p => ['-v', `${p}:${p}`]), + ...['HOME', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN'].flatMap(e => ['-e', e]), + '-w', fixture.integTestDir, + '--cap-add=NET_ADMIN', + 'public.ecr.aws/ubuntu/ubuntu:24.04_stable', + `${scriptName}`, + ], { + modEnv: { + AWS_ACCESS_KEY_ID: credentials.accessKeyId, + AWS_SECRET_ACCESS_KEY: credentials.secretAccessKey, + AWS_SESSION_TOKEN: credentials.sessionToken, + }, + }); + } finally { + await proxy.stop(); + } +} + +/** + * Return the commands necessary to isolate the inside of the container from the internet, + * except by going through the proxy + */ +function isolatedDockerCommands(proxyPort: number, caBundlePath: string) { + let defaultBridgeIp = '172.17.0.1'; + + // The host's default IP is different on CodeBuild than it is on other Docker systems. + if (process.env.CODEBUILD_BUILD_ARN) { + defaultBridgeIp = '172.18.0.1'; + } + + return [ + 'echo Working...', + 'apt-get install -qqy curl net-tools iputils-ping dnsutils iptables > /dev/null', + '', + // Looking up `host.docker.internal` is necessary on MacOS Finch and Docker Desktop + // on Mac. The magic IP address is necessary on CodeBuild and GitHub Actions. + // + // I tried `--add-host=host.docker.internal:host-gateway` on the Linuxes but + // it doesn't change anything on either GHA or CodeBuild, so we're left with this + // backup solution. + 'gateway=$(dig +short host.docker.internal)', + 'if [[ -z "$gateway" ]]; then', + ` gateway=${defaultBridgeIp}`, + 'fi', + '', + '# Some iptables manipulation; there might be unnecessary commands in here, not an expert', + 'iptables -F', + 'iptables -X', + 'iptables -P INPUT DROP', + 'iptables -P OUTPUT DROP', + 'iptables -P FORWARD DROP', + 'iptables -A INPUT -i lo -j ACCEPT', + 'iptables -A OUTPUT -o lo -j ACCEPT', + 'iptables -A OUTPUT -d $gateway -j ACCEPT', + 'iptables -A INPUT -s $gateway -j ACCEPT', + '', + '', + `if [[ ! -f ${caBundlePath} ]]; then`, + ` echo "Could not find ${caBundlePath}, this will probably not go well. Exiting." >&2`, + ' exit 1', + 'fi', + '', + '# Configure a bunch of tools to work with the proxy', + 'echo "+-------------------------------------------------------------------------------------+"', + 'echo "| Direct network traffic has been blocked, everything must go through the proxy. |"', + 'echo "+-------------------------------------------------------------------------------------+"', + `export HTTP_PROXY=http://$gateway:${proxyPort}/`, + `export HTTPS_PROXY=http://$gateway:${proxyPort}/`, + `export NODE_EXTRA_CA_CERTS=${caBundlePath}`, + `export AWS_CA_BUNDLE=${caBundlePath}`, + `export SSL_CERT_FILE=${caBundlePath}`, + 'echo "Acquire::http::proxy \"$HTTP_PROXY\";" >> /etc/apt/apt.conf.d/95proxies', + 'echo "Acquire::https::proxy \"$HTTPS_PROXY\";" >> /etc/apt/apt.conf.d/95proxies', + ]; +} + +function renderEnv(env: Record) { + return Object.entries(env).filter(([_, v]) => v).map(([k, v]) => `${k}='${v}'`).join(' '); +} + +/** + * Find a broadly mountable parent of the given directory + * + * We don't want to just mount the top-level directory, because + * it could end up being `/var` (top-level dir of tmpdir on Mac). + */ +function findMountableParent(dir: string): string { + const candidates = [ + os.tmpdir(), + process.env.TMPDIR, + process.env.HOME, + ]; + for (const cand of candidates) { + if (cand === undefined) { + continue; + } + + if (dir.startsWith(cand)) { + const suffix = dir.substring(cand.length); + const firstChildDir = suffix.split('/')[0]; + + return path.join(cand, firstChildDir); + } + } + return dir; +} diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-csharp/init-csharp.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-csharp/init-csharp.integtest.ts new file mode 100644 index 000000000..192726470 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-csharp/init-csharp.integtest.ts @@ -0,0 +1,15 @@ +import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '../../lib'; + +['app', 'sample-app'].forEach(template => { + integTest(`init C♯ ${template}`, withTemporaryDirectory(withPackages(async (context) => { + context.packages.assertJsiiPackagesAvailable(); + + const shell = ShellHelper.fromContext(context); + await context.packages.makeCliAvailable(); + + await shell.shell(['cdk', 'init', '-l', 'csharp', template]); + await context.packages.initializeDotnetPackages(context.integTestDir); + await shell.shell(['cdk', 'synth']); + }))); +}); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-fsharp/init-fsharp.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-fsharp/init-fsharp.integtest.ts new file mode 100644 index 000000000..fdc1fca2d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-fsharp/init-fsharp.integtest.ts @@ -0,0 +1,15 @@ +import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '../../lib'; + +['app', 'sample-app'].forEach(template => { + integTest(`init F♯ ${template}`, withTemporaryDirectory(withPackages(async (context) => { + context.packages.assertJsiiPackagesAvailable(); + + const shell = ShellHelper.fromContext(context); + await context.packages.makeCliAvailable(); + + await shell.shell(['cdk', 'init', '-l', 'fsharp', template]); + await context.packages.initializeDotnetPackages(context.integTestDir); + await shell.shell(['cdk', 'synth']); + }))); +}); + diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-go/init-go.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-go/init-go.integtest.ts new file mode 100644 index 000000000..ee5d9418f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-go/init-go.integtest.ts @@ -0,0 +1,28 @@ +import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '../../lib'; + +['app', 'sample-app'].forEach(template => { + integTest(`init go ${template}`, withTemporaryDirectory(withPackages(async (context) => { + const isCanary = !!process.env.IS_CANARY; + context.packages.assertJsiiPackagesAvailable(); + + const shell = ShellHelper.fromContext(context); + await context.packages.makeCliAvailable(); + + await shell.shell(['cdk', 'init', '-l', 'go', template]); + + // Canaries will use the generated go.mod as is + // For pipeline tests we replace the source with the locally build one + if (!isCanary) { + const dir = process.env.CODEBUILD_SRC_DIR ?? process.env.GITHUB_WORKSPACE; + if (!dir) { + throw new Error('Cannot figure out CI system root directory'); + } + + await shell.shell(['go', 'mod', 'edit', '-replace', `github.com/aws/aws-cdk-go/awscdk/v2=${dir}/go/awscdk`]); + } + + await shell.shell(['go', 'mod', 'tidy']); + await shell.shell(['go', 'test']); + await shell.shell(['cdk', 'synth']); + }))); +}); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-java/init-java.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-java/init-java.integtest.ts new file mode 100644 index 000000000..48d5b38b1 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-java/init-java.integtest.ts @@ -0,0 +1,14 @@ +import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '../../lib'; + +['app', 'sample-app'].forEach(template => { + integTest(`init java ${template}`, withTemporaryDirectory(withPackages(async (context) => { + context.packages.assertJsiiPackagesAvailable(); + + const shell = ShellHelper.fromContext(context); + await context.packages.makeCliAvailable(); + + await shell.shell(['cdk', 'init', '-l', 'java', template]); + await shell.shell(['mvn', 'package']); + await shell.shell(['cdk', 'synth']); + }))); +}); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts new file mode 100644 index 000000000..b189df2b1 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-javascript/init-javascript.integtest.ts @@ -0,0 +1,58 @@ +import * as path from 'path'; +import * as fs from 'fs-extra'; +import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '../../lib'; + +['app', 'sample-app'].forEach(template => { + integTest(`init javascript ${template}`, withTemporaryDirectory(withPackages(async (context) => { + const shell = ShellHelper.fromContext(context); + await context.packages.makeCliAvailable(); + + await shell.shell(['cdk', 'init', '-l', 'javascript', template]); + await shell.shell(['npm', 'prune']); + await shell.shell(['npm', 'ls']); // this will fail if we have unmet peer dependencies + await shell.shell(['npm', 'run', 'test']); + + await shell.shell(['cdk', 'synth']); + }))); +}); + +integTest('Test importing CDK from ESM', withTemporaryDirectory(withPackages(async (context) => { + // Use 'cdk init -l=javascript' to get set up, but use a different file + const shell = ShellHelper.fromContext(context); + await context.packages.makeCliAvailable(); + + await shell.shell(['cdk', 'init', '-l', 'javascript', 'app']); + + // Rewrite some files + await fs.writeFile(path.join(context.integTestDir, 'new-entrypoint.mjs'), ` +// Test multiple styles of imports +import { Stack, aws_sns as sns } from 'aws-cdk-lib'; +import { SqsSubscription } from 'aws-cdk-lib/aws-sns-subscriptions'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import * as cdk from 'aws-cdk-lib'; + +class TestjsStack extends Stack { + constructor(scope, id, props) { + super(scope, id, props); + + const queue = new sqs.Queue(this, 'TestjsQueue', { + visibilityTimeout: cdk.Duration.seconds(300) + }); + + const topic = new sns.Topic(this, 'TestjsTopic'); + + topic.addSubscription(new SqsSubscription(queue)); + } +} + +const app = new cdk.App(); +new TestjsStack(app, 'TestjsStack'); +`, { encoding: 'utf-8' }); + + // Rewrite 'cdk.json' to use new entrypoint + const cdkJson = await fs.readJson(path.join(context.integTestDir, 'cdk.json')); + cdkJson.app = 'node new-entrypoint.mjs'; + await fs.writeJson(path.join(context.integTestDir, 'cdk.json'), cdkJson); + + await shell.shell(['cdk', 'synth']); +}))); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts new file mode 100644 index 000000000..06d725d5a --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-python/init-python.integtest.ts @@ -0,0 +1,20 @@ +import * as path from 'path'; +import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '../../lib'; + +['app', 'sample-app'].forEach(template => { + integTest(`init python ${template}`, withTemporaryDirectory(withPackages(async (context) => { + context.packages.assertJsiiPackagesAvailable(); + + const shell = ShellHelper.fromContext(context); + await context.packages.makeCliAvailable(); + + await shell.shell(['cdk', 'init', '-l', 'python', template]); + const venvPath = path.resolve(context.integTestDir, '.venv'); + const venv = { PATH: `${venvPath}/bin:${process.env.PATH}`, VIRTUAL_ENV: venvPath }; + + await shell.shell([`${venvPath}/bin/pip`, 'install', '-r', 'requirements.txt'], { modEnv: venv }); + await shell.shell([`${venvPath}/bin/pip`, 'install', '-r', 'requirements-dev.txt'], { modEnv: venv }); + await shell.shell([`${venvPath}/bin/pytest`], { modEnv: venv }); + await shell.shell(['cdk', 'synth'], { modEnv: venv }); + }))); +}); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts new file mode 100644 index 000000000..92800944c --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-app/init-typescript-app.integtest.ts @@ -0,0 +1,68 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import type { TemporaryDirectoryContext } from '../../lib'; +import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '../../lib'; +import { typescriptVersionsSync, typescriptVersionsYoungerThanDaysSync } from '../../lib/npm'; + +['app', 'sample-app'].forEach(template => { + integTest(`typescript init ${template}`, withTemporaryDirectory(withPackages(async (context) => { + const shell = ShellHelper.fromContext(context); + await context.packages.makeCliAvailable(); + + await shell.shell(['cdk', 'init', '-l', 'typescript', template]); + + await shell.shell(['npm', 'prune']); + await shell.shell(['npm', 'ls']); // this will fail if we have unmet peer dependencies + await shell.shell(['npm', 'run', 'build']); + await shell.shell(['npm', 'run', 'test']); + + await shell.shell(['cdk', 'synth']); + }))); +}); + +// Same as https://github.com/DefinitelyTyped/DefinitelyTyped?tab=readme-ov-file#support-window +const TYPESCRIPT_VERSION_AGE_DAYS = 2 * 365; + +const TYPESCRIPT_VERSIONS = typescriptVersionsYoungerThanDaysSync(TYPESCRIPT_VERSION_AGE_DAYS, typescriptVersionsSync()); + +/** + * Test our generated code with various versions of TypeScript + */ +TYPESCRIPT_VERSIONS.forEach(tsVersion => { + integTest(`typescript ${tsVersion} init app`, withTemporaryDirectory(withPackages(async (context) => { + const shell = ShellHelper.fromContext(context); + await context.packages.makeCliAvailable(); + + await shell.shell(['node', '--version']); + await shell.shell(['npm', '--version']); + + await shell.shell(['cdk', 'init', '-l', 'typescript', 'app', '--generate-only']); + + // Necessary because recent versions of ts-jest require TypeScript>=4.3 but we + // still want to test with older versions as well. + await removeDevDependencies(context); + + await shell.shell(['npm', 'install', '--save-dev', `typescript@${tsVersion}`]); + + // After we've removed devDependencies we need to re-install ts-node because it's necessary for `cdk synth` + await shell.shell(['npm', 'install', '--save-dev', 'ts-node@^10']); + + await shell.shell(['npm', 'install']); // Older versions of npm require this to be a separate step from the one above + await shell.shell(['npx', 'tsc', '--version']); + await shell.shell(['npm', 'prune']); + await shell.shell(['npm', 'ls']); // this will fail if we have unmet peer dependencies + + // We just removed the 'jest' dependency so remove the tests as well because they won't compile + await shell.shell(['rm', '-rf', 'test/']); + + await shell.shell(['npm', 'run', 'build']); + await shell.shell(['cdk', 'synth']); + }))); +}); + +async function removeDevDependencies(context: TemporaryDirectoryContext) { + const filename = path.join(context.integTestDir, 'package.json'); + const pj = JSON.parse(await fs.readFile(filename, { encoding: 'utf-8' })); + delete pj.devDependencies; + await fs.writeFile(filename, JSON.stringify(pj, undefined, 2), { encoding: 'utf-8' }); +} diff --git a/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/init-typescript-lib.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/init-typescript-lib.integtest.ts new file mode 100644 index 000000000..eaee28500 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/init-typescript-lib/init-typescript-lib.integtest.ts @@ -0,0 +1,13 @@ +import { integTest, withTemporaryDirectory, ShellHelper, withPackages } from '../../lib'; + +integTest('typescript init lib', withTemporaryDirectory(withPackages(async (context) => { + const shell = ShellHelper.fromContext(context); + await context.packages.makeCliAvailable(); + + await shell.shell(['cdk', 'init', '-l', 'typescript', 'lib']); + + await shell.shell(['npm', 'prune']); + await shell.shell(['npm', 'ls']); // this will fail if we have unmet peer dependencies + await shell.shell(['npm', 'run', 'build']); + await shell.shell(['npm', 'run', 'test']); +}))); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/amplify.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/amplify.integtest.ts new file mode 100644 index 000000000..348584c4f --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/amplify.integtest.ts @@ -0,0 +1,118 @@ +import { promises as fs } from 'fs'; +import * as path from 'path'; +import { withToolContext } from './with-tool-context'; +import type { TemporaryDirectoryContext } from '../../lib'; +import { integTest, ShellHelper } from '../../lib'; + +const TIMEOUT = 1800_000; + +integTest('amplify integration', withToolContext(async (context) => { + const shell = ShellHelper.fromContext(context); + + //////////////////////////////////////////////////////////////////////// + // Make sure that create-amplify installs the right versions of the CLI and framework + // + + // Install `create-amplify` without running it, then hack the json file with the + // package versions in it before we execute. + await shell.shell(['npm', 'init', '-y']); + await shell.shell(['npm', 'install', '--save-dev', 'create-amplify@latest']); + // This will create 'package.json' implicating a certain version of the CDK + await shell.shell(['npm', 'config', 'set', 'save-exact', 'true']); + await mutateAmplifyDepOnCdk(context, context.packages.requestedCliVersion(), context.packages.requestedFrameworkVersion()); + + //////////////////////////////////////////////////////////////////////// + // Run the `npm create` workflow + // + + // I tested to confirm that this will use the locally installed `create-amplify` + await shell.shell(['npm', 'create', '-y', 'amplify']); + await shell.shell(['npx', 'ampx', 'configure', 'telemetry', 'disable']); + + const awsCreds = context.aws.identityEnv(); + + await shell.shell(['npx', 'ampx', 'sandbox', '--once'], { + modEnv: { + AWS_REGION: context.aws.region, + ...awsCreds, + }, + }); + try { + + // Future code goes here, putting the try/finally here already so it doesn't + // get forgotten. + + } finally { + await shell.shell(['npx', 'ampx', 'sandbox', 'delete', '--yes'], { + modEnv: { + AWS_REGION: context.aws.region, + ...awsCreds, + }, + }); + } +}), TIMEOUT); + +async function mutateAmplifyDepOnCdk(context: TemporaryDirectoryContext, cliVersion: string, libVersion: string) { + // default_packages.json is where create-amplify reads when installing npm dependencies + const amplifyDepFile = path.join(context.integTestDir, 'node_modules', 'create-amplify', 'lib', 'default_packages.json'); + const amplifyDepJson: unknown = JSON.parse(await fs.readFile(amplifyDepFile, { encoding: 'utf-8' })); + + // Be extra paranoid about the types here, since we don't fully control them + assertIsObject(amplifyDepJson); + assertIsStringArray(amplifyDepJson.defaultDevPackages); + + // Amplify is removing the dependency on aws-cdk, since Amplify is now using the toolkit-lib + // To prepare for this change, we need allow both situations: aws-cdk being listed and not being listed + // Fix is to simply allow the replace operation to also NOT replace the version + // @see https://github.com/aws-amplify/amplify-backend/pull/2614 + replacePackageVersionIn('aws-cdk', cliVersion, amplifyDepJson.defaultDevPackages, false); + replacePackageVersionIn('aws-cdk-lib', libVersion, amplifyDepJson.defaultDevPackages); + + await fs.writeFile(amplifyDepFile, JSON.stringify(amplifyDepJson, undefined, 2), { encoding: 'utf-8' }); + + const packageJsonFile = path.join(context.integTestDir, 'package.json'); + const packageJson: unknown = JSON.parse(await fs.readFile(packageJsonFile, { encoding: 'utf-8' })); + + assertIsObject(packageJson); + packageJson.overrides = { + 'aws-cdk-lib': libVersion, + }; + await fs.writeFile(packageJsonFile, JSON.stringify(packageJson, undefined, 2), { encoding: 'utf-8' }); +} + +/** + * Mutably update the given string array, replacing the version of packages with the given name + * + * We assume the list of packages is a string array of the form + * + * ``` + * ["package@version", "package@version", ...] + * ``` + * + * It's a failure if we don't find an entry to update, unless we explicitly pass an option to say that's okay. + */ +function replacePackageVersionIn(packName: string, version: string, xs: string[], failIfMissing = true) { + let didUpdate = false; + for (let i = 0; i < xs.length; i++) { + if (xs[i].startsWith(`${packName}@`)) { + xs[i] = `${packName}@${version}`; + didUpdate = true; + } + } + + if (failIfMissing && !didUpdate) { + throw new Error(`Did not find a package version to update for ${packName} in ${JSON.stringify(xs)}`); + } +} + +function assertIsObject(xs: unknown): asserts xs is Record { + if (typeof xs !== 'object' || xs === null) { + throw new Error(`Expected object, got ${JSON.stringify(xs)}`); + } +} + +function assertIsStringArray(xs: unknown): asserts xs is string[] { + if (!Array.isArray(xs) || xs.length === 0 || typeof xs[0] !== 'string') { + throw new Error(`Expected list of strings, got ${JSON.stringify(xs)}`); + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/with-tool-context.ts b/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/with-tool-context.ts new file mode 100644 index 000000000..e35faf6b6 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/tool-integrations/with-tool-context.ts @@ -0,0 +1,17 @@ +import type { TestContext } from '../../lib/integ-test'; +import type { AwsContext } from '../../lib/with-aws'; +import { withAws } from '../../lib/with-aws'; +import type { DisableBootstrapContext } from '../../lib/with-cdk-app'; +import type { PackageContext } from '../../lib/with-packages'; +import { withPackages } from '../../lib/with-packages'; +import type { TemporaryDirectoryContext } from '../../lib/with-temporary-directory'; +import { withTemporaryDirectory } from '../../lib/with-temporary-directory'; + +/** + * The default prerequisites for tests running tool integrations + */ +export function withToolContext( + block: (context: A & TemporaryDirectoryContext & PackageContext & AwsContext & DisableBootstrapContext + ) => Promise) { + return withAws(withTemporaryDirectory(withPackages(block))); +} diff --git a/packages/@aws-cdk-testing/cli-integ/tests/uberpackage/uberpackage.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/uberpackage/uberpackage.integtest.ts new file mode 100644 index 000000000..f9000516b --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/uberpackage/uberpackage.integtest.ts @@ -0,0 +1,11 @@ +import { integTest, withSpecificFixture } from '../../lib'; + +jest.setTimeout(600_000); + +describe('uberpackage', () => { + integTest('works with cloudformation-include', withSpecificFixture('cfn-include-app', async (fixture) => { + fixture.log('Starting test of cfn-include with monolithic CDK'); + + await fixture.cdkSynth(); + })); +}); diff --git a/packages/@aws-cdk-testing/cli-integ/tsconfig.dev.json b/packages/@aws-cdk-testing/cli-integ/tsconfig.dev.json new file mode 100644 index 000000000..60945b2b6 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tsconfig.dev.json @@ -0,0 +1,42 @@ +// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". +{ + "compilerOptions": { + "alwaysStrict": true, + "declaration": true, + "esModuleInterop": false, + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": [ + "es2020" + ], + "module": "commonjs", + "noEmitOnError": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2020", + "incremental": true, + "skipLibCheck": true, + "composite": true, + "outDir": "lib" + }, + "include": [ + "./**/*.ts", + "test/**/*.ts", + "**/*.ts" + ], + "exclude": [ + "node_modules", + "resources/**/*" + ], + "references": [] +} diff --git a/packages/@aws-cdk-testing/cli-integ/tsconfig.json b/packages/@aws-cdk-testing/cli-integ/tsconfig.json new file mode 100644 index 000000000..a5f939ac4 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tsconfig.json @@ -0,0 +1,41 @@ +// ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". +{ + "compilerOptions": { + "rootDir": ".", + "outDir": ".", + "alwaysStrict": true, + "declaration": true, + "esModuleInterop": false, + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": [ + "es2020" + ], + "module": "commonjs", + "noEmitOnError": false, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "target": "ES2020", + "incremental": true, + "skipLibCheck": true, + "composite": true + }, + "include": [ + "./**/*.ts", + "**/*.ts" + ], + "exclude": [ + "resources/**/*" + ], + "references": [] +} diff --git a/packages/@aws-cdk/cdk-cli-wrapper/package.json b/packages/@aws-cdk/cdk-cli-wrapper/package.json index b0c40fe05..19cc31067 100644 --- a/packages/@aws-cdk/cdk-cli-wrapper/package.json +++ b/packages/@aws-cdk/cdk-cli-wrapper/package.json @@ -39,7 +39,7 @@ "constructs": "^10.0.0", "eslint": "^9", "eslint-config-prettier": "^10.1.2", - "eslint-import-resolver-typescript": "^4.3.2", + "eslint-import-resolver-typescript": "^4.3.3", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.11.0", "eslint-plugin-jsdoc": "^50.6.9", diff --git a/packages/@aws-cdk/cdk-cli-wrapper/tsconfig.dev.json b/packages/@aws-cdk/cdk-cli-wrapper/tsconfig.dev.json index c23fc6c66..5bfbfa299 100644 --- a/packages/@aws-cdk/cdk-cli-wrapper/tsconfig.dev.json +++ b/packages/@aws-cdk/cdk-cli-wrapper/tsconfig.dev.json @@ -8,8 +8,7 @@ "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2020", - "dom" + "es2020" ], "module": "commonjs", "noEmitOnError": false, diff --git a/packages/@aws-cdk/cdk-cli-wrapper/tsconfig.json b/packages/@aws-cdk/cdk-cli-wrapper/tsconfig.json index 494d42c75..4fe81a0dd 100644 --- a/packages/@aws-cdk/cdk-cli-wrapper/tsconfig.json +++ b/packages/@aws-cdk/cdk-cli-wrapper/tsconfig.json @@ -10,8 +10,7 @@ "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2020", - "dom" + "es2020" ], "module": "commonjs", "noEmitOnError": false, diff --git a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json index 84fd761a1..a766252e8 100644 --- a/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json +++ b/packages/@aws-cdk/cli-lib-alpha/.projen/tasks.json @@ -61,7 +61,7 @@ "name": "check-licenses", "steps": [ { - "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC\"", + "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC;BSD-3-Clause\"", "receiveArgs": true } ] diff --git a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES index a9a28d3d7..176483276 100644 --- a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES +++ b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES @@ -2266,7 +2266,7 @@ The @aws-cdk/cli-lib-alpha package includes the following third-party software/l ---------------- -** @aws-sdk/client-ecs@3.787.0 - https://www.npmjs.com/package/@aws-sdk/client-ecs/v/3.787.0 | Apache-2.0 +** @aws-sdk/client-ecs@3.791.0 - https://www.npmjs.com/package/@aws-sdk/client-ecs/v/3.791.0 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/packages/@aws-cdk/cli-lib-alpha/package.json b/packages/@aws-cdk/cli-lib-alpha/package.json index 7e2272b8b..d9d708658 100644 --- a/packages/@aws-cdk/cli-lib-alpha/package.json +++ b/packages/@aws-cdk/cli-lib-alpha/package.json @@ -46,7 +46,7 @@ "@typescript-eslint/eslint-plugin": "^8", "@typescript-eslint/parser": "^8", "aws-cdk": "^0.0.0", - "aws-cdk-lib": "^2.189.0", + "aws-cdk-lib": "^2.190.0", "commit-and-tag-version": "^12", "constructs": "^10.0.0", "eslint": "^9", diff --git a/packages/@aws-cdk/cli-lib-alpha/tsconfig.dev.json b/packages/@aws-cdk/cli-lib-alpha/tsconfig.dev.json index 1f69e055f..281fa13a8 100644 --- a/packages/@aws-cdk/cli-lib-alpha/tsconfig.dev.json +++ b/packages/@aws-cdk/cli-lib-alpha/tsconfig.dev.json @@ -8,8 +8,7 @@ "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2020", - "dom" + "es2020" ], "module": "commonjs", "noEmitOnError": false, diff --git a/packages/@aws-cdk/cli-plugin-contract/.projen/tasks.json b/packages/@aws-cdk/cli-plugin-contract/.projen/tasks.json index 05f8a15c9..931a85a01 100644 --- a/packages/@aws-cdk/cli-plugin-contract/.projen/tasks.json +++ b/packages/@aws-cdk/cli-plugin-contract/.projen/tasks.json @@ -59,7 +59,7 @@ "name": "check-licenses", "steps": [ { - "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC\"", + "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC;BSD-3-Clause\"", "receiveArgs": true } ] diff --git a/packages/@aws-cdk/cli-plugin-contract/tsconfig.dev.json b/packages/@aws-cdk/cli-plugin-contract/tsconfig.dev.json index c23fc6c66..5bfbfa299 100644 --- a/packages/@aws-cdk/cli-plugin-contract/tsconfig.dev.json +++ b/packages/@aws-cdk/cli-plugin-contract/tsconfig.dev.json @@ -8,8 +8,7 @@ "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2020", - "dom" + "es2020" ], "module": "commonjs", "noEmitOnError": false, diff --git a/packages/@aws-cdk/cli-plugin-contract/tsconfig.json b/packages/@aws-cdk/cli-plugin-contract/tsconfig.json index 494d42c75..4fe81a0dd 100644 --- a/packages/@aws-cdk/cli-plugin-contract/tsconfig.json +++ b/packages/@aws-cdk/cli-plugin-contract/tsconfig.json @@ -10,8 +10,7 @@ "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2020", - "dom" + "es2020" ], "module": "commonjs", "noEmitOnError": false, diff --git a/packages/@aws-cdk/cloud-assembly-schema/.projen/tasks.json b/packages/@aws-cdk/cloud-assembly-schema/.projen/tasks.json index d61ffe699..36a85cfc8 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/.projen/tasks.json +++ b/packages/@aws-cdk/cloud-assembly-schema/.projen/tasks.json @@ -60,7 +60,7 @@ "name": "check-licenses", "steps": [ { - "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC\"", + "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC;BSD-3-Clause\"", "receiveArgs": true } ] diff --git a/packages/@aws-cdk/cloudformation-diff/.projen/tasks.json b/packages/@aws-cdk/cloudformation-diff/.projen/tasks.json index 588414ff0..398941231 100644 --- a/packages/@aws-cdk/cloudformation-diff/.projen/tasks.json +++ b/packages/@aws-cdk/cloudformation-diff/.projen/tasks.json @@ -60,7 +60,7 @@ "name": "check-licenses", "steps": [ { - "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC\"", + "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC;BSD-3-Clause\"", "receiveArgs": true } ] diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index eb49cbc18..464fd621f 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -57,8 +57,8 @@ "typescript": "5.6" }, "dependencies": { - "@aws-cdk/aws-service-spec": "^0.1.67", - "@aws-cdk/service-spec-types": "^0.0.133", + "@aws-cdk/aws-service-spec": "^0.1.69", + "@aws-cdk/service-spec-types": "^0.0.135", "chalk": "^4", "diff": "^7.0.0", "fast-deep-equal": "^3.1.3", diff --git a/packages/@aws-cdk/cloudformation-diff/tsconfig.dev.json b/packages/@aws-cdk/cloudformation-diff/tsconfig.dev.json index c23fc6c66..5bfbfa299 100644 --- a/packages/@aws-cdk/cloudformation-diff/tsconfig.dev.json +++ b/packages/@aws-cdk/cloudformation-diff/tsconfig.dev.json @@ -8,8 +8,7 @@ "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2020", - "dom" + "es2020" ], "module": "commonjs", "noEmitOnError": false, diff --git a/packages/@aws-cdk/cloudformation-diff/tsconfig.json b/packages/@aws-cdk/cloudformation-diff/tsconfig.json index 494d42c75..4fe81a0dd 100644 --- a/packages/@aws-cdk/cloudformation-diff/tsconfig.json +++ b/packages/@aws-cdk/cloudformation-diff/tsconfig.json @@ -10,8 +10,7 @@ "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2020", - "dom" + "es2020" ], "module": "commonjs", "noEmitOnError": false, diff --git a/packages/@aws-cdk/integ-runner/.projen/tasks.json b/packages/@aws-cdk/integ-runner/.projen/tasks.json index 6c9406e1b..7cfb73d5f 100644 --- a/packages/@aws-cdk/integ-runner/.projen/tasks.json +++ b/packages/@aws-cdk/integ-runner/.projen/tasks.json @@ -59,7 +59,7 @@ "name": "check-licenses", "steps": [ { - "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC\"", + "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC;BSD-3-Clause\"", "receiveArgs": true } ] diff --git a/packages/@aws-cdk/integ-runner/lib/recommended-feature-flags.json b/packages/@aws-cdk/integ-runner/lib/recommended-feature-flags.json index 585c8534d..3b6f870cb 100644 --- a/packages/@aws-cdk/integ-runner/lib/recommended-feature-flags.json +++ b/packages/@aws-cdk/integ-runner/lib/recommended-feature-flags.json @@ -68,5 +68,7 @@ "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, "@aws-cdk/aws-events:requireEventBusPolicySid": true, - "@aws-cdk/aws-dynamodb:retainTableReplica": true + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true } \ No newline at end of file diff --git a/packages/@aws-cdk/integ-runner/lib/runner/runner-base.d.ts b/packages/@aws-cdk/integ-runner/lib/runner/runner-base.d.ts index 35d5a358d..db0678756 100644 --- a/packages/@aws-cdk/integ-runner/lib/runner/runner-base.d.ts +++ b/packages/@aws-cdk/integ-runner/lib/runner/runner-base.d.ts @@ -283,5 +283,7 @@ export declare function currentlyRecommendedAwsCdkLibFlags(): { "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": boolean; "@aws-cdk/aws-s3:setUniqueReplicationRoleName": boolean; "@aws-cdk/aws-events:requireEventBusPolicySid": boolean; + "@aws-cdk/core:aspectPrioritiesMutating": boolean; "@aws-cdk/aws-dynamodb:retainTableReplica": boolean; + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": boolean; }; diff --git a/packages/@aws-cdk/integ-runner/package.json b/packages/@aws-cdk/integ-runner/package.json index 0d601fdc8..eb73e58e7 100644 --- a/packages/@aws-cdk/integ-runner/package.json +++ b/packages/@aws-cdk/integ-runner/package.json @@ -45,12 +45,12 @@ "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8", "@typescript-eslint/parser": "^8", - "aws-cdk-lib": "^2.189.0", + "aws-cdk-lib": "^2.190.0", "commit-and-tag-version": "^12", "constructs": "^10", "eslint": "^9", "eslint-config-prettier": "^10.1.2", - "eslint-import-resolver-typescript": "^4.3.2", + "eslint-import-resolver-typescript": "^4.3.3", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.11.0", "eslint-plugin-jsdoc": "^50.6.9", @@ -65,11 +65,11 @@ "typescript": "5.6" }, "dependencies": { - "@aws-cdk/aws-service-spec": "^0.1.67", + "@aws-cdk/aws-service-spec": "^0.1.69", "@aws-cdk/cdk-cli-wrapper": "^0.0.0", "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/cloudformation-diff": "^0.0.0", - "@aws-cdk/cx-api": "^2.189.0", + "@aws-cdk/cx-api": "^2.190.0", "@aws-sdk/client-cloudformation": "^3", "aws-cdk": "^0.0.0", "cdk-assets": "^0.0.0", diff --git a/packages/@aws-cdk/node-bundle/tsconfig.dev.json b/packages/@aws-cdk/node-bundle/tsconfig.dev.json index 8c3d5c245..7d0394b1d 100644 --- a/packages/@aws-cdk/node-bundle/tsconfig.dev.json +++ b/packages/@aws-cdk/node-bundle/tsconfig.dev.json @@ -8,8 +8,7 @@ "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2020", - "dom" + "es2020" ], "module": "commonjs", "noEmitOnError": false, diff --git a/packages/@aws-cdk/node-bundle/tsconfig.json b/packages/@aws-cdk/node-bundle/tsconfig.json index 1594d817e..15be92e41 100644 --- a/packages/@aws-cdk/node-bundle/tsconfig.json +++ b/packages/@aws-cdk/node-bundle/tsconfig.json @@ -10,8 +10,7 @@ "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2020", - "dom" + "es2020" ], "module": "commonjs", "noEmitOnError": false, diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/.gitattributes b/packages/@aws-cdk/tmp-toolkit-helpers/.gitattributes index c1b26c9d0..f8b7d2522 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/.gitattributes +++ b/packages/@aws-cdk/tmp-toolkit-helpers/.gitattributes @@ -15,6 +15,7 @@ /jest.config.json linguist-generated /LICENSE linguist-generated /package.json linguist-generated +/test/tsconfig.json linguist-generated /tsconfig.dev.json linguist-generated /tsconfig.json linguist-generated /yarn.lock linguist-generated \ No newline at end of file diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/.gitignore b/packages/@aws-cdk/tmp-toolkit-helpers/.gitignore index 5945d3dc1..3e68013f5 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/.gitignore +++ b/packages/@aws-cdk/tmp-toolkit-helpers/.gitignore @@ -41,4 +41,5 @@ jspm_packages/ /dist/ !/.eslintrc.json !/.eslintrc.js +!/test/tsconfig.json test/**/*.map diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/.projen/files.json b/packages/@aws-cdk/tmp-toolkit-helpers/.projen/files.json index 493bbd87e..9b2047f9d 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/.projen/files.json +++ b/packages/@aws-cdk/tmp-toolkit-helpers/.projen/files.json @@ -12,6 +12,7 @@ ".projen/tasks.json", "jest.config.json", "LICENSE", + "test/tsconfig.json", "tsconfig.dev.json", "tsconfig.json" ], diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/.projen/tasks.json b/packages/@aws-cdk/tmp-toolkit-helpers/.projen/tasks.json index dc4de6664..47d86faae 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/.projen/tasks.json +++ b/packages/@aws-cdk/tmp-toolkit-helpers/.projen/tasks.json @@ -48,6 +48,9 @@ { "exec": "tsc --build", "receiveArgs": true + }, + { + "exec": "tsc --build test" } ] }, diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/package.json b/packages/@aws-cdk/tmp-toolkit-helpers/package.json index 22af32b1d..f9fb8a6cb 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/package.json +++ b/packages/@aws-cdk/tmp-toolkit-helpers/package.json @@ -52,7 +52,7 @@ "fast-check": "^3.23.2", "jest": "^29.7.0", "jest-junit": "^16", - "nock": "^14.0.3", + "nock": "^14.0.4", "prettier": "^2.8", "projen": "^0.91.20", "ts-jest": "^29.3.2", @@ -62,7 +62,7 @@ "dependencies": { "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/cloudformation-diff": "^0.0.0", - "@aws-cdk/cx-api": "^2.189.0", + "@aws-cdk/cx-api": "^2.190.0", "@aws-sdk/client-appsync": "^3", "@aws-sdk/client-cloudcontrol": "^3", "@aws-sdk/client-cloudformation": "^3", diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/awscli-compatible.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/awscli-compatible.ts index 1ff4ae2a2..8cb34aed3 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/awscli-compatible.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/awscli-compatible.ts @@ -24,9 +24,22 @@ const DEFAULT_TIMEOUT = 300000; */ export class AwsCliCompatible { private readonly ioHelper: IoHelper; + private readonly requestHandler: NodeHttpHandlerOptions; + private readonly logger?: Logger; - public constructor(ioHelper: IoHelper) { + public constructor(ioHelper: IoHelper, requestHandler: NodeHttpHandlerOptions, logger?: Logger) { this.ioHelper = ioHelper; + this.requestHandler = requestHandler; + this.logger = logger; + } + + public async baseConfig(profile?: string): Promise<{ credentialProvider: AwsCredentialIdentityProvider; defaultRegion: string }> { + const credentialProvider = await this.credentialChainBuilder({ + profile, + logger: this.logger, + }); + const defaultRegion = await this.region(profile); + return { credentialProvider, defaultRegion }; } /** @@ -38,7 +51,7 @@ export class AwsCliCompatible { options: CredentialChainOptions = {}, ): Promise { const clientConfig = { - requestHandler: await this.requestHandlerBuilder(options.httpOptions), + requestHandler: this.requestHandler, customUserAgent: 'aws-cdk', logger: options.logger, }; @@ -115,17 +128,6 @@ export class AwsCliCompatible { : nodeProviderChain; } - public async requestHandlerBuilder(options: SdkHttpOptions = {}): Promise { - const agent = await new ProxyAgentProvider(this.ioHelper).create(options); - - return { - connectionTimeout: DEFAULT_CONNECTION_TIMEOUT, - requestTimeout: DEFAULT_TIMEOUT, - httpsAgent: agent, - httpAgent: agent, - }; - } - /** * 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. @@ -265,6 +267,16 @@ function shouldPrioritizeEnv() { export interface CredentialChainOptions { readonly profile?: string; - readonly httpOptions?: SdkHttpOptions; readonly logger?: Logger; } + +export async function makeRequestHandler(ioHelper: IoHelper, options: SdkHttpOptions = {}): Promise { + const agent = await new ProxyAgentProvider(ioHelper).create(options); + + return { + connectionTimeout: DEFAULT_CONNECTION_TIMEOUT, + requestTimeout: DEFAULT_TIMEOUT, + httpsAgent: agent, + httpAgent: agent, + }; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/credential-plugins.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/credential-plugins.ts index 453c2d6ad..58ddbd95b 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/credential-plugins.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/credential-plugins.ts @@ -4,8 +4,8 @@ import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@smit import { credentialsAboutToExpire, makeCachingProvider } from './provider-caching'; import { formatErrorMessage } from '../../util'; import { IO, type IoHelper } from '../io/private'; +import type { PluginHost } from '../plugin'; import type { Mode } from '../plugin/mode'; -import type { PluginHost } from '../plugin/plugin'; import { AuthenticationError } from '../toolkit-error'; /** @@ -22,12 +22,8 @@ import { AuthenticationError } from '../toolkit-error'; */ export class CredentialPlugins { private readonly cache: { [key: string]: PluginCredentialsFetchResult | undefined } = {}; - private readonly host: PluginHost; - private readonly ioHelper: IoHelper; - constructor(host: PluginHost, ioHelper: IoHelper) { - this.host = host; - this.ioHelper = ioHelper; + constructor(private readonly host: PluginHost, private readonly ioHelper: IoHelper) { } public async fetchCredentialsFor(awsAccountId: string, mode: Mode): Promise { diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/sdk-provider.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/sdk-provider.ts index 2146e8cc7..7b011b7db 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/sdk-provider.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/aws-auth/sdk-provider.ts @@ -14,7 +14,7 @@ import { SDK } from './sdk'; import { callTrace, traceMemberMethods } from './tracing'; import { formatErrorMessage } from '../../util'; import { IO, type IoHelper } from '../io/private'; -import { Mode, PluginHost } from '../plugin'; +import { PluginHost, Mode } from '../plugin'; import { AuthenticationError } from '../toolkit-error'; export type AssumeRoleAdditionalOptions = Partial>; @@ -22,28 +22,13 @@ export type AssumeRoleAdditionalOptions = Partial { return cached(this, CACHED_ACCOUNT, async () => { try { - return await new SDK(this.defaultCredentialProvider, this.defaultRegion, this.requestHandler, this.ioHelper, this.logger).currentAccount(); + return await this._makeSdk(this.defaultCredentialProvider, this.defaultRegion).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 @@ -384,7 +366,7 @@ export class SdkProvider { // Call the provider at least once here, to catch an error if it occurs await credentials(); - return new SDK(credentials, region, this.requestHandler, this.ioHelper, this.logger); + return this._makeSdk(credentials, region); } catch (err: any) { if (err.name === 'ExpiredToken') { throw err; @@ -402,6 +384,29 @@ export class SdkProvider { ); } } + + /** + * Factory function that creates a new SDK instance + * + * This is a function here, instead of all the places where this is used creating a `new SDK` + * instance, so that it is trivial to mock from tests. + * + * Use like this: + * + * ```ts + * const mockSdk = jest.spyOn(SdkProvider.prototype, '_makeSdk').mockReturnValue(new MockSdk()); + * // ... + * mockSdk.mockRestore(); + * ``` + * + * @internal + */ + public _makeSdk( + credProvider: AwsCredentialIdentityProvider, + region: string, + ) { + return new SDK(credProvider, region, this.requestHandler, this.ioHelper, this.logger); + } } /** @@ -541,3 +546,25 @@ export async function initContextProviderSdk(aws: SdkProvider, options: ContextL return (await aws.forEnvironment(EnvironmentUtils.make(account, region), Mode.ForReading, creds)).sdk; } + +export interface SdkProviderServices { + /** + * An IO helper for emitting messages + */ + readonly ioHelper: IoHelper; + + /** + * The request handler settings + */ + readonly requestHandler?: NodeHttpHandlerOptions; + + /** + * A plugin host + */ + readonly pluginHost?: PluginHost; + + /** + * An SDK logger + */ + readonly logger?: Logger; +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/plugin/plugin.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/plugin/plugin.ts index ac9c7cb79..971757498 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/api/plugin/plugin.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/api/plugin/plugin.ts @@ -1,6 +1,8 @@ import { inspect } from 'util'; import type { CredentialProviderSource, IPluginHost, Plugin } from '@aws-cdk/cli-plugin-contract'; import { type ContextProviderPlugin, isContextProviderPlugin } from './context-provider-plugin'; +import type { IIoHost } from '../io'; +import { IoDefaultMessages, IoHelper } from '../private'; import { ToolkitError } from '../toolkit-error'; export let TESTING = false; @@ -10,12 +12,13 @@ export function markTesting() { } /** - * A utility to manage plug-ins. + * Class to manage a plugin collection * + * It provides a `load()` function that loads a JavaScript + * module from disk, and gives it access to the `IPluginHost` interface + * to register itself. */ export class PluginHost implements IPluginHost { - public static instance = new PluginHost(); - /** * Access the currently registered CredentialProviderSources. New sources can * be registered using the +registerCredentialProviderSource+ method. @@ -24,30 +27,60 @@ export class PluginHost implements IPluginHost { public readonly contextProviderPlugins: Record = {}; - constructor() { - if (!TESTING && PluginHost.instance && PluginHost.instance !== this) { - throw new ToolkitError('New instances of PluginHost must not be built. Use PluginHost.instance instead!'); - } - } + public ioHost?: IIoHost; + + private readonly alreadyLoaded = new Set(); /** * Loads a plug-in into this PluginHost. * + * Will use `require.resolve()` to get the most accurate representation of what + * code will get loaded in error messages. As such, it will not work in + * unit tests with Jest virtual modules becauase of . + * * @param moduleSpec the specification (path or name) of the plug-in module to be loaded. + * @param ioHost the I/O host to use for printing progress information */ - public load(moduleSpec: string) { + public load(moduleSpec: string, ioHost?: IIoHost) { try { + const resolved = require.resolve(moduleSpec); + if (ioHost) { + new IoDefaultMessages(IoHelper.fromIoHost(ioHost, 'init')).debug(`Loading plug-in: ${resolved} from ${moduleSpec}`); + } + return this._doLoad(resolved); + } catch (e: any) { + // according to Node.js docs `MODULE_NOT_FOUND` is the only possible error here + // @see https://nodejs.org/api/modules.html#requireresolverequest-options + // Not using `withCause()` here, since the node error contains a "Require Stack" + // as part of the error message that is inherently useless to our users. + throw new ToolkitError(`Unable to resolve plug-in: Cannot find module '${moduleSpec}': ${e}`); + } + } + + /** + * Do the loading given an already-resolved module name + * + * @internal + */ + public _doLoad(resolved: string) { + try { + if (this.alreadyLoaded.has(resolved)) { + return; + } + /* eslint-disable @typescript-eslint/no-require-imports */ - const plugin = require(moduleSpec); + const plugin = require(resolved); /* eslint-enable */ if (!isPlugin(plugin)) { - throw new ToolkitError(`Module ${moduleSpec} is not a valid plug-in, or has an unsupported version.`); + throw new ToolkitError(`Module ${resolved} is not a valid plug-in, or has an unsupported version.`); } if (plugin.init) { plugin.init(this); } + + this.alreadyLoaded.add(resolved); } catch (e: any) { - throw ToolkitError.withCause(`Unable to load plug-in '${moduleSpec}'`, e); + throw ToolkitError.withCause(`Unable to load plug-in '${resolved}'`, e); } function isPlugin(x: any): x is Plugin { diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/src/context-providers/index.ts b/packages/@aws-cdk/tmp-toolkit-helpers/src/context-providers/index.ts index d008d24b6..d2569503c 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/src/context-providers/index.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/src/context-providers/index.ts @@ -16,8 +16,7 @@ import { TRANSIENT_CONTEXT_KEY } from '../api/context'; import { replaceEnvPlaceholders } from '../api/environment'; import { IO } from '../api/io/private'; import type { IoHelper } from '../api/io/private'; -import { PluginHost } from '../api/plugin'; -import type { ContextProviderPlugin } from '../api/plugin'; +import type { PluginHost, ContextProviderPlugin } from '../api/plugin'; import { ContextProviderError } from '../api/toolkit-error'; import { formatErrorMessage } from '../util'; @@ -72,6 +71,7 @@ export async function provideContextValues( missingValues: cxschema.MissingContext[], context: Context, sdk: SdkProvider, + pluginHost: PluginHost, ioHelper: IoHelper, ) { for (const missingContext of missingValues) { @@ -83,7 +83,7 @@ export async function provideContextValues( let factory; if (providerName.startsWith(`${PLUGIN_PROVIDER_PREFIX}:`)) { - const plugin = PluginHost.instance.contextProviderPlugins[providerName.substring(PLUGIN_PROVIDER_PREFIX.length + 1)]; + const plugin = pluginHost.contextProviderPlugins[providerName.substring(PLUGIN_PROVIDER_PREFIX.length + 1)]; if (!plugin) { // eslint-disable-next-line max-len throw new ContextProviderError(`Unrecognized plugin context provider name: ${missingContext.provider}.`); diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/test/_helpers/jest-bufferedconsole.ts b/packages/@aws-cdk/tmp-toolkit-helpers/test/_helpers/jest-bufferedconsole.ts index 043b819ec..3569a60ca 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/test/_helpers/jest-bufferedconsole.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/test/_helpers/jest-bufferedconsole.ts @@ -24,7 +24,7 @@ export default class TestEnvironment extends NodeEnvironment implements JestEnvi // We need to set the event handler by assignment in the constructor, // because if we declare it as an async member TypeScript's type derivation // doesn't work properly. - (this as JestEnvironment).handleTestEvent = (async (event, _state) => { + (this as JestEnvironment).handleTestEvent = (async (event) => { if (event.name === 'test_done' && event.test.errors.length > 0 && this.log.length > 0) { this.stopCapture(); diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/test/api/diff/diff.test.ts b/packages/@aws-cdk/tmp-toolkit-helpers/test/api/diff/diff.test.ts index 23e9db33d..bfe0bcd66 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/test/api/diff/diff.test.ts +++ b/packages/@aws-cdk/tmp-toolkit-helpers/test/api/diff/diff.test.ts @@ -62,7 +62,6 @@ describe('formatStackDiff', () => { templateInfo: { oldTemplate: mockNewTemplate.template, newTemplate: mockNewTemplate, - stackName: 'test-stack', }, }); const result = formatter.formatStackDiff(); @@ -84,7 +83,6 @@ describe('formatStackDiff', () => { templateInfo: { oldTemplate: {}, newTemplate: mockNewTemplate, - stackName: 'test-stack', }, }); const result = formatter.formatStackDiff(); @@ -107,7 +105,6 @@ describe('formatStackDiff', () => { templateInfo: { oldTemplate: {}, newTemplate: mockNewTemplate, - stackName: 'test-stack', isImport: true, }, }); @@ -149,7 +146,6 @@ describe('formatStackDiff', () => { templateInfo: { oldTemplate: {}, newTemplate: mockNewTemplate, - stackName: 'test-stack', nestedStacks, }, }); @@ -225,7 +221,6 @@ describe('formatSecurityDiff', () => { templateInfo: { oldTemplate: mockNewTemplate.template, newTemplate: mockNewTemplate, - stackName: 'test-stack', }, }); const result = formatter.formatSecurityDiff({ @@ -244,7 +239,6 @@ describe('formatSecurityDiff', () => { templateInfo: { oldTemplate: {}, newTemplate: mockNewTemplate, - stackName: 'test-stack', }, }); const result = formatter.formatSecurityDiff({ @@ -279,7 +273,6 @@ describe('formatSecurityDiff', () => { templateInfo: { oldTemplate: {}, newTemplate: mockNewTemplate, - stackName: 'test-stack', }, }); const result = formatter.formatSecurityDiff({ @@ -317,7 +310,6 @@ describe('formatSecurityDiff', () => { templateInfo: { oldTemplate: {}, newTemplate: mockNewTemplate, - stackName: 'test-stack', }, }); const result = formatter.formatSecurityDiff({ diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/test/tsconfig.json b/packages/@aws-cdk/tmp-toolkit-helpers/test/tsconfig.json new file mode 100644 index 000000000..fab093523 --- /dev/null +++ b/packages/@aws-cdk/tmp-toolkit-helpers/test/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.dev.json", + "compilerOptions": { + "rootDir": "..", + "noEmit": true + }, + "references": [ + { + "path": "../../cloudformation-diff" + }, + { + "path": "../../../cdk-assets" + } + ], + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." +} diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/tsconfig.dev.json b/packages/@aws-cdk/tmp-toolkit-helpers/tsconfig.dev.json index 4d5b4c04d..7b3921c5e 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/tsconfig.dev.json +++ b/packages/@aws-cdk/tmp-toolkit-helpers/tsconfig.dev.json @@ -24,7 +24,7 @@ "strict": true, "strictNullChecks": true, "strictPropertyInitialization": true, - "stripInternal": true, + "stripInternal": false, "target": "es2022", "incremental": true, "skipLibCheck": true, diff --git a/packages/@aws-cdk/tmp-toolkit-helpers/tsconfig.json b/packages/@aws-cdk/tmp-toolkit-helpers/tsconfig.json index 5bb516730..08d50cc35 100644 --- a/packages/@aws-cdk/tmp-toolkit-helpers/tsconfig.json +++ b/packages/@aws-cdk/tmp-toolkit-helpers/tsconfig.json @@ -26,7 +26,7 @@ "strict": true, "strictNullChecks": true, "strictPropertyInitialization": true, - "stripInternal": true, + "stripInternal": false, "target": "es2022", "incremental": true, "skipLibCheck": true, diff --git a/packages/@aws-cdk/toolkit-lib/.gitattributes b/packages/@aws-cdk/toolkit-lib/.gitattributes index 1a8feca5e..b154bcbe3 100644 --- a/packages/@aws-cdk/toolkit-lib/.gitattributes +++ b/packages/@aws-cdk/toolkit-lib/.gitattributes @@ -16,6 +16,7 @@ /jest.config.json linguist-generated /LICENSE linguist-generated /package.json linguist-generated +/test/tsconfig.json linguist-generated /tsconfig.dev.json linguist-generated /tsconfig.json linguist-generated /yarn.lock linguist-generated \ No newline at end of file diff --git a/packages/@aws-cdk/toolkit-lib/.gitignore b/packages/@aws-cdk/toolkit-lib/.gitignore index 65b677450..cc61a9bb2 100644 --- a/packages/@aws-cdk/toolkit-lib/.gitignore +++ b/packages/@aws-cdk/toolkit-lib/.gitignore @@ -45,6 +45,7 @@ jspm_packages/ /dist/changelog.md /dist/version.txt !/.eslintrc.js +!/test/tsconfig.json !/api-extractor.json db.json.gz .init-version.json diff --git a/packages/@aws-cdk/toolkit-lib/.projen/files.json b/packages/@aws-cdk/toolkit-lib/.projen/files.json index fb4daac5e..fe16b7122 100644 --- a/packages/@aws-cdk/toolkit-lib/.projen/files.json +++ b/packages/@aws-cdk/toolkit-lib/.projen/files.json @@ -13,6 +13,7 @@ "api-extractor.json", "jest.config.json", "LICENSE", + "test/tsconfig.json", "tsconfig.dev.json", "tsconfig.json" ], diff --git a/packages/@aws-cdk/toolkit-lib/.projen/tasks.json b/packages/@aws-cdk/toolkit-lib/.projen/tasks.json index fedc81c11..1902df395 100644 --- a/packages/@aws-cdk/toolkit-lib/.projen/tasks.json +++ b/packages/@aws-cdk/toolkit-lib/.projen/tasks.json @@ -67,7 +67,7 @@ "name": "check-licenses", "steps": [ { - "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC\"", + "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC;BSD-3-Clause\"", "receiveArgs": true } ] @@ -79,6 +79,9 @@ { "exec": "tsc --build", "receiveArgs": true + }, + { + "exec": "tsc --build test" } ] }, diff --git a/packages/@aws-cdk/toolkit-lib/README.md b/packages/@aws-cdk/toolkit-lib/README.md index e79b056ed..66ea6d08e 100644 --- a/packages/@aws-cdk/toolkit-lib/README.md +++ b/packages/@aws-cdk/toolkit-lib/README.md @@ -402,7 +402,7 @@ declare const cx: ICloudAssemblySource; try { // Attempt a CDK Toolkit operation - const deployment = await cdk.deploy(cloudAssembly, { + const deployment = await cdk.deploy(cx, { stacks: ['MyStack'] }); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/types.ts b/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/types.ts index b95fad8c1..98e3162f9 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/types.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/types.ts @@ -1,12 +1,23 @@ +import type { AwsCredentialIdentityProvider } from '@smithy/types'; +import type { SdkProviderServices } from '../shared-private'; +import { AwsCliCompatible } from '../shared-private'; /** * Options for the default SDK provider */ export interface SdkConfig { /** - * Profile to read from ~/.aws + * The base credentials and region used to seed the Toolkit with + * + * @default BaseCredentials.awsCliCompatible() + */ + readonly baseCredentials?: BaseCredentials; + + /** + * Profile to read from ~/.aws for base credentials * * @default - No profile + * @deprecated Use `baseCredentials` instead */ readonly profile?: string; @@ -34,3 +45,123 @@ export interface SdkHttpOptions { */ readonly caBundlePath?: string; } + +export abstract class BaseCredentials { + /** + * Use no base credentials + * + * There will be no current account and no current region during synthesis. To + * successfully deploy with this set of base credentials: + * + * - The CDK app must provide concrete accounts and regions during synthesis + * - Credential plugins must be installed to provide credentials for those + * accounts. + */ + public static none(): BaseCredentials { + return new class extends BaseCredentials { + public async makeSdkConfig(): Promise { + return { + credentialProvider: () => { + // eslint-disable-next-line @cdklabs/no-throw-default-error + throw new Error('No credentials available due to BaseCredentials.none()'); + }, + }; + } + + public toString() { + return 'BaseCredentials.none()'; + } + }; + } + + /** + * Obtain base credentials and base region the same way the AWS CLI would + * + * Credentials and region will be read from the environment first, falling back + * to INI files or other sources if available. + * + * The profile name is configurable. + */ + public static awsCliCompatible(options: AwsCliCompatibleOptions = {}): BaseCredentials { + return new class extends BaseCredentials { + public makeSdkConfig(services: SdkProviderServices): Promise { + const awsCli = new AwsCliCompatible(services.ioHelper, services.requestHandler ?? {}, services.logger); + return awsCli.baseConfig(options.profile); + } + + public toString() { + return `BaseCredentials.awsCliCompatible(${JSON.stringify(options)})`; + } + }; + } + + /** + * Use a custom SDK identity provider for the base credentials + * + * If your provider uses STS calls to obtain base credentials, you must make + * sure to also configure the necessary HTTP options (like proxy and user + * agent) and the region on the STS client directly; the toolkit code cannot + * do this for you. + */ + public static custom(options: CustomBaseCredentialsOption): BaseCredentials { + return new class extends BaseCredentials { + public makeSdkConfig(): Promise { + return Promise.resolve({ + credentialProvider: options.provider, + defaultRegion: options.region, + }); + } + + public toString() { + return `BaseCredentials.custom(${JSON.stringify({ + ...options, + provider: '...', + })})`; + } + }; + } + + /** + * Make SDK config from the BaseCredentials settings + */ + public abstract makeSdkConfig(services: SdkProviderServices): Promise; +} + +export interface AwsCliCompatibleOptions { + /** + * The profile to read from `~/.aws/credentials`. + * + * If not supplied the environment variable AWS_PROFILE will be used. + * + * @default - Use environment variable if set. + */ + readonly profile?: string; +} + +export interface CustomBaseCredentialsOption { + /** + * The credentials provider to use to obtain base credentials + * + * If your provider uses STS calls to obtain base credentials, you must make + * sure to also configure the necessary HTTP options (like proxy and user + * agent) on the STS client directly; the toolkit code cannot do this for you. + */ + readonly provider: AwsCredentialIdentityProvider; + + /** + * The default region to synthesize for + * + * CDK applications can override this region. NOTE: this region will *not* + * affect any STS calls made by the given provider, if any. You need to configure + * your credential provider separately. + * + * @default 'us-east-1' + */ + readonly region?: string; +} + +export interface SdkBaseConfig { + readonly credentialProvider: AwsCredentialIdentityProvider; + + readonly defaultRegion?: string; +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts index a01a11290..43c489e9f 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/context-aware-source.ts @@ -103,6 +103,7 @@ export class ContextAwareCloudAssemblySource implements ICloudAssemblySource { assembly.manifest.missing, this.context, this.props.services.sdkProvider, + this.props.services.pluginHost, this.ioHelper, ); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts index 8720c0eb8..944dac40c 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/cloud-assembly/private/source-builder.ts @@ -57,9 +57,9 @@ export abstract class CloudAssemblySourceBuilder { const env = await execution.defaultEnvVars(); const assembly = await execution.changeDir(async () => execution.withContext(context.all, env, props.synthOptions ?? {}, async (envWithContext, ctx) => - execution.withEnv(envWithContext, () => { + execution.withEnv(envWithContext, async () => { try { - return builder({ + return await builder({ outdir: execution.outdir, context: ctx, }); diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/shared-private.ts b/packages/@aws-cdk/toolkit-lib/lib/api/shared-private.ts index 89d57cfc9..58062e547 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/shared-private.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/shared-private.ts @@ -4,6 +4,7 @@ export * from '../../../tmp-toolkit-helpers/src/api/io/private'; export * from '../../../tmp-toolkit-helpers/src/private'; export * from '../../../tmp-toolkit-helpers/src/api'; export * as cfnApi from '../../../tmp-toolkit-helpers/src/api/deployments/cfn-api'; +export { makeRequestHandler } from '../../../tmp-toolkit-helpers/src/api/aws-auth/awscli-compatible'; // Context Providers export * as contextproviders from '../../../tmp-toolkit-helpers/src/context-providers'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/shared-public.ts b/packages/@aws-cdk/toolkit-lib/lib/api/shared-public.ts index 4e046903f..129dc679a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/shared-public.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/shared-public.ts @@ -20,5 +20,6 @@ export type { } from '../../../tmp-toolkit-helpers/src/api/io/io-message'; export type { IIoHost } from '../../../tmp-toolkit-helpers/src/api/io/io-host'; export type { ToolkitAction } from '../../../tmp-toolkit-helpers/src/api/io/toolkit-action'; +export { PluginHost } from '../../../tmp-toolkit-helpers/src/api/plugin/plugin'; export * from '../../../tmp-toolkit-helpers/src/payloads'; diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts index f45ba473b..cb2a6f90e 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/private/index.ts @@ -1,7 +1,7 @@ import type { ICloudAssemblySource } from '../../api/cloud-assembly'; import { StackAssembly } from '../../api/cloud-assembly/private'; -import type { SdkProvider, IoHelper } from '../../api/shared-private'; +import type { SdkProvider, IoHelper, PluginHost } from '../../api/shared-private'; /** * Helper struct to pass internal services around. @@ -9,6 +9,7 @@ import type { SdkProvider, IoHelper } from '../../api/shared-private'; export interface ToolkitServices { sdkProvider: SdkProvider; ioHelper: IoHelper; + pluginHost: PluginHost; } /** diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index a89aaad82..35c017179 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -32,7 +32,7 @@ import { type RollbackOptions } from '../actions/rollback'; import { type SynthOptions } from '../actions/synth'; import type { WatchOptions } from '../actions/watch'; import { patternsArrayForWatch } from '../actions/watch/private'; -import { type SdkConfig } from '../api/aws-auth'; +import { BaseCredentials, type SdkConfig } from '../api/aws-auth'; import type { ICloudAssemblySource } from '../api/cloud-assembly'; import { CachedCloudAssembly, StackSelectionStrategy } from '../api/cloud-assembly'; import type { StackAssembly } from '../api/cloud-assembly/private'; @@ -47,8 +47,10 @@ import type { StackCollection, StackNode, SuccessfulDeployStackResult, + SdkProviderServices, } from '../api/shared-private'; import { + SdkProvider, AmbiguityError, ambiguousMovements, asIoHelper, @@ -64,13 +66,14 @@ import { HotswapMode, RequireApproval, ResourceMigrator, - SdkProvider, tagsForStack, ToolkitError, resourceMappings, WorkGraphBuilder, + makeRequestHandler, } from '../api/shared-private'; import type { AssemblyData, StackDetails, ToolkitAction } from '../api/shared-public'; +import { PluginHost } from '../api/shared-public'; import { formatErrorMessage, formatTime, @@ -122,6 +125,17 @@ export interface ToolkitOptions { * @default "error" */ readonly assemblyFailureAt?: 'error' | 'warn' | 'none'; + + /** + * The plugin host to use for loading and querying plugins + * + * By default, a unique instance of a plugin managing class will be used. + * + * Use `toolkit.pluginHost.load()` to load plugins into the plugin host from disk. + * + * @default - A fresh plugin host + */ + readonly pluginHost?: PluginHost; } /** @@ -138,15 +152,24 @@ export class Toolkit extends CloudAssemblySourceBuilder { */ public readonly ioHost: IIoHost; + /** + * The plugin host for loading and managing plugins + */ + public readonly pluginHost: PluginHost; + /** * Cache of the internal SDK Provider instance */ private sdkProviderCache?: SdkProvider; + private baseCredentials: BaseCredentials; + public constructor(private readonly props: ToolkitOptions = {}) { super(); this.toolkitStackName = props.toolkitStackName ?? DEFAULT_TOOLKIT_STACK_NAME; + this.pluginHost = props.pluginHost ?? new PluginHost(); + let ioHost = props.ioHost ?? new NonInteractiveIoHost(); if (props.emojis === false) { ioHost = withoutEmojis(ioHost); @@ -157,6 +180,11 @@ export class Toolkit extends CloudAssemblySourceBuilder { // After removing emojis and color, we might end up with floating whitespace at either end of the message // This also removes newlines that we currently emit for CLI backwards compatibility. this.ioHost = withTrimmedWhitespace(ioHost); + + if (props.sdkConfig?.profile && props.sdkConfig?.baseCredentials) { + throw new ToolkitError('Specify at most one of \'sdkConfig.profile\' and \'sdkConfig.baseCredentials\''); + } + this.baseCredentials = props.sdkConfig?.baseCredentials ?? BaseCredentials.awsCliCompatible({ profile: props.sdkConfig?.profile }); } /** @@ -167,11 +195,15 @@ export class Toolkit extends CloudAssemblySourceBuilder { // @todo this needs to be different instance per action if (!this.sdkProviderCache) { const ioHelper = asIoHelper(this.ioHost, action); - this.sdkProviderCache = await SdkProvider.withAwsCliCompatibleDefaults({ - ...this.props.sdkConfig, + const services: SdkProviderServices = { ioHelper, + requestHandler: await makeRequestHandler(ioHelper, this.props.sdkConfig?.httpOptions), logger: asSdkLogger(ioHelper), - }); + pluginHost: this.pluginHost, + }; + + const config = await this.baseCredentials.makeSdkConfig(services); + this.sdkProviderCache = new SdkProvider(config.credentialProvider, config.defaultRegion, services); } return this.sdkProviderCache; @@ -185,6 +217,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { return { ioHelper: asIoHelper(this.ioHost, 'assembly'), sdkProvider: await this.sdkProvider('assembly'), + pluginHost: this.pluginHost, }; } @@ -910,7 +943,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { }); } catch (e: any) { await ioHelper.notify(IO.CDK_TOOLKIT_E6900.msg(`\n ❌ ${chalk.bold(stack.displayName)} failed: ${formatErrorMessage(e)}`, { error: e })); - throw new ToolkitError('Rollback failed (use --force to orphan failing resources)'); + throw ToolkitError.withCause('Rollback failed (use --force to orphan failing resources)', e); } } if (!anyRollbackable) { diff --git a/packages/@aws-cdk/toolkit-lib/package.json b/packages/@aws-cdk/toolkit-lib/package.json index 473601098..d5f7b1c1d 100644 --- a/packages/@aws-cdk/toolkit-lib/package.json +++ b/packages/@aws-cdk/toolkit-lib/package.json @@ -35,10 +35,10 @@ "organization": true }, "devDependencies": { - "@aws-cdk/aws-service-spec": "^0.1.67", + "@aws-cdk/aws-service-spec": "^0.1.69", "@aws-cdk/tmp-toolkit-helpers": "^0.0.0", "@cdklabs/eslint-plugin": "^1.3.2", - "@microsoft/api-extractor": "^7.52.3", + "@microsoft/api-extractor": "^7.52.4", "@smithy/types": "^4.2.0", "@stylistic/eslint-plugin": "^3", "@types/fs-extra": "^11.0.4", @@ -47,7 +47,7 @@ "@types/split2": "^4.2.3", "@typescript-eslint/eslint-plugin": "^8", "@typescript-eslint/parser": "^8", - "aws-cdk-lib": "^2.189.0", + "aws-cdk-lib": "^2.190.0", "aws-sdk-client-mock": "^4.1.0", "aws-sdk-client-mock-jest": "^4.1.0", "commit-and-tag-version": "^12", @@ -67,14 +67,14 @@ "prettier": "^2.8", "projen": "^0.91.20", "ts-jest": "^29.3.2", - "typedoc": "^0.28.2", + "typedoc": "^0.28.3", "typescript": "5.6" }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/cloudformation-diff": "^0.0.0", - "@aws-cdk/cx-api": "^2.189.0", - "@aws-cdk/region-info": "^2.189.0", + "@aws-cdk/cx-api": "^2.190.0", + "@aws-cdk/region-info": "^2.190.0", "@aws-sdk/client-appsync": "^3", "@aws-sdk/client-cloudcontrol": "^3", "@aws-sdk/client-cloudformation": "^3", @@ -106,7 +106,7 @@ "archiver": "^7.0.1", "camelcase": "^6", "cdk-assets": "^0.0.0", - "cdk-from-cfn": "^0.206.0", + "cdk-from-cfn": "^0.210.0", "chalk": "^4", "chokidar": "^3", "decamelize": "^5", diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-defined-env/app.js b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-defined-env/app.js new file mode 100644 index 000000000..dea4bfd63 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-defined-env/app.js @@ -0,0 +1,12 @@ +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as core from 'aws-cdk-lib/core'; + +const app = new core.App({ autoSynth: false }); +const stack = new core.Stack(app, 'Stack1', { + env: { + account: '11111111111', + region: 'us-east-1', + }, +}); +new s3.Bucket(stack, 'MyBucket'); +app.synth(); diff --git a/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-env-from-env/app.js b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-env-from-env/app.js new file mode 100644 index 000000000..c3daf0976 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/_fixtures/stack-with-env-from-env/app.js @@ -0,0 +1,12 @@ +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as core from 'aws-cdk-lib/core'; + +const app = new core.App({ autoSynth: false }); +const stack = new core.Stack(app, 'Stack1', { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, +}); +new s3.Bucket(stack, 'MyBucket'); +app.synth(); diff --git a/packages/@aws-cdk/toolkit-lib/test/_helpers/mock-sdk.ts b/packages/@aws-cdk/toolkit-lib/test/_helpers/mock-sdk.ts index 9f5d0c6d3..f87cb2640 100644 --- a/packages/@aws-cdk/toolkit-lib/test/_helpers/mock-sdk.ts +++ b/packages/@aws-cdk/toolkit-lib/test/_helpers/mock-sdk.ts @@ -144,7 +144,9 @@ export const setDefaultSTSMocks = () => { */ export class MockSdkProvider extends SdkProvider { constructor() { - super(FAKE_CREDENTIAL_CHAIN, 'bermuda-triangle-1337', {}, new TestIoHost().asHelper('sdk')); + super(FAKE_CREDENTIAL_CHAIN, 'bermuda-triangle-1337', { + ioHelper: new TestIoHost().asHelper('sdk'), + }); } public defaultAccount(): Promise { @@ -162,6 +164,13 @@ export class MockSdk extends SDK { constructor() { super(FAKE_CREDENTIAL_CHAIN, 'bermuda-triangle-1337', {}, new TestIoHost().asHelper('sdk')); } + + public async currentAccount(): Promise { + return { + accountId: '123456789012', + partition: 'aws', + }; + } } export function mockBootstrapStack(stack?: Partial) { diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts index aad1a081c..45bcbcfa9 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/bootstrap.test.ts @@ -16,7 +16,6 @@ import { SdkProvider } from '../../lib/api/shared-private'; import { Toolkit } from '../../lib/toolkit'; import { TestIoHost, builderFixture, disposableCloudAssemblySource } from '../_helpers'; import { - MockSdkProvider, MockSdk, mockCloudFormationClient, restoreSdkMocksToDefault, @@ -25,18 +24,16 @@ import { const ioHost = new TestIoHost(); const toolkit = new Toolkit({ ioHost }); -const mockSdkProvider = new MockSdkProvider(); - -// we don't need to use AWS CLI compatible defaults here, since everything is mocked anyway -jest.spyOn(SdkProvider, 'withAwsCliCompatibleDefaults').mockResolvedValue(mockSdkProvider); beforeEach(() => { restoreSdkMocksToDefault(); setDefaultSTSMocks(); ioHost.notifySpy.mockClear(); - mockSdkProvider.forEnvironment = jest.fn().mockImplementation(() => { - return { sdk: new MockSdk() }; + jest.spyOn(SdkProvider.prototype, '_makeSdk').mockReturnValue(new MockSdk()); + jest.spyOn(SdkProvider.prototype, 'forEnvironment').mockResolvedValue({ + sdk: new MockSdk(), + didAssumeRole: false, }); }); diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts index 8d137f22d..79f0afdd2 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts @@ -6,6 +6,7 @@ import { RequireApproval } from '../../lib/api/shared-private'; import { StackSelectionStrategy } from '../../lib/api/shared-public'; import { Toolkit } from '../../lib/toolkit'; import { builderFixture, disposableCloudAssemblySource, TestIoHost } from '../_helpers'; +import { MockSdk } from '../_helpers/mock-sdk'; let ioHost: TestIoHost; let toolkit: Toolkit; @@ -18,6 +19,8 @@ beforeEach(() => { toolkit = new Toolkit({ ioHost }); // Some default implementations + jest.spyOn(apis.SdkProvider.prototype, '_makeSdk').mockReturnValue(new MockSdk()); + jest.spyOn(apis.Deployments.prototype, 'readCurrentTemplateWithNestedStacks').mockResolvedValue({ deployedRootTemplate: { Parameters: {}, diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/refactor.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/refactor.test.ts index 58197b1cc..9341fcad8 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/refactor.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/refactor.test.ts @@ -2,17 +2,15 @@ import { GetTemplateCommand, ListStacksCommand } from '@aws-sdk/client-cloudform import { StackSelectionStrategy, Toolkit } from '../../lib'; import { SdkProvider } from '../../lib/api/shared-private'; import { builderFixture, TestIoHost } from '../_helpers'; -import { mockCloudFormationClient, MockSdkProvider } from '../_helpers/mock-sdk'; +import { mockCloudFormationClient, MockSdk } from '../_helpers/mock-sdk'; // these tests often run a bit longer than the default jest.setTimeout(10_000); const ioHost = new TestIoHost(); const toolkit = new Toolkit({ ioHost }); -const mockSdkProvider = new MockSdkProvider(); -// we don't need to use AWS CLI compatible defaults here, since everything is mocked anyway -jest.spyOn(SdkProvider, 'withAwsCliCompatibleDefaults').mockResolvedValue(mockSdkProvider); +jest.spyOn(SdkProvider.prototype, '_makeSdk').mockReturnValue(new MockSdk()); beforeEach(() => { ioHost.notifySpy.mockClear(); diff --git a/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts index 14472fd3c..34d8edb30 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/cloud-assembly/source-builder.test.ts @@ -37,11 +37,17 @@ describe('fromAssemblyBuilder', () => { expect(JSON.stringify(stack)).toContain('amzn-s3-demo-bucket'); }); - test('errors are wrapped as AssemblyError', async () => { + test.each(['sync', 'async'] as const)('errors are wrapped as AssemblyError for %s builder', async (sync) => { // GIVEN - const cx = await toolkit.fromAssemblyBuilder(() => { - throw new Error('a wild error appeared'); - }); + const builder = sync === 'sync' + ? () => { + throw new Error('a wild error appeared'); + } + : async () => { + throw new Error('a wild error appeared'); + }; + + const cx = await toolkit.fromAssemblyBuilder(builder); // WHEN try { @@ -82,18 +88,18 @@ describe('fromAssemblyBuilder', () => { // GIVEN const cx = await toolkit.fromAssemblyBuilder(async (props) => { lock = new RWLock(props.outdir!); - if (!await lock._currentWriter()) { + if (!await (lock as any)._currentWriter()) { throw new Error('Expected the directory to be locked during synth'); } throw new Error('a wild error appeared'); }); // WHEN - await expect(cx.produce()).rejects.toThrow(/wild error/); + await expect(cx.produce()).rejects.toThrow(); // THEN: Don't expect either a read or write lock on the directory afterwards - expect(await lock!._currentWriter()).toBeUndefined(); - expect(await lock!._currentReaders()).toEqual([]); + expect(await (lock! as any)._currentWriter()).toBeUndefined(); + expect(await (lock! as any)._currentReaders()).toEqual([]); }); }); @@ -162,8 +168,8 @@ describe('fromCdkApp', () => { await expect(cx.produce()).rejects.toThrow(/error 1/); // THEN: Don't expect either a read or write lock on the directory afterwards - expect(await lock!._currentWriter()).toBeUndefined(); - expect(await lock!._currentReaders()).toEqual([]); + expect(await (lock! as any)._currentWriter()).toBeUndefined(); + expect(await (lock! as any)._currentReaders()).toEqual([]); }); }); diff --git a/packages/@aws-cdk/toolkit-lib/test/toolkit/base-credentials.test.ts b/packages/@aws-cdk/toolkit-lib/test/toolkit/base-credentials.test.ts new file mode 100644 index 000000000..b3286ae28 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/toolkit/base-credentials.test.ts @@ -0,0 +1,74 @@ +import { BaseCredentials } from '../../lib/api/aws-auth/types'; +import { SdkProvider } from '../../lib/api/shared-private'; +import { Toolkit } from '../../lib/toolkit/toolkit'; +import { appFixture, TestIoHost } from '../_helpers'; +import { MockSdk } from '../_helpers/mock-sdk'; + +let ioHost: TestIoHost; +let makeSdk: jest.SpiedFunction; + +beforeEach(() => { + jest.restoreAllMocks(); + ioHost = new TestIoHost(); + makeSdk = jest.spyOn(SdkProvider.prototype, '_makeSdk').mockReturnValue(new MockSdk()); +}); + +test('custom credentials can be used for synth', async () => { + const customProvider = async () => ({ accessKeyId: 'a', secretAccessKey: 's' }); + const toolkit = new Toolkit({ + ioHost, + sdkConfig: { + baseCredentials: BaseCredentials.custom({ + provider: customProvider, + region: 'south-pole-1', + }), + }, + }); + + const cx = await appFixture(toolkit, 'stack-with-env-from-env'); + await using asm = await toolkit.synth(cx); + + expect(asm.cloudAssembly.getStackByName('Stack1').environment).toMatchObject({ + account: '123456789012', // Returned from the MockSdk + region: 'south-pole-1', // Configured by the BaseCredentials + }); + expect(makeSdk).toHaveBeenCalledWith(customProvider, expect.anything()); +}); + +test('none credentials can be used for a self-defining stack', async () => { + const toolkit = new Toolkit({ + ioHost, + sdkConfig: { + baseCredentials: BaseCredentials.none(), + }, + }); + + const cx = await appFixture(toolkit, 'stack-with-defined-env'); + await using asm = await toolkit.synth(cx); + + expect(asm.cloudAssembly.getStackByName('Stack1').environment).toMatchObject({ + account: '11111111111', + region: 'us-east-1', + }); +}); + +test.each([ + [BaseCredentials.awsCliCompatible({ profile: 'this-profile-doesnt-exist' }), true], + [BaseCredentials.custom({ provider: () => Promise.resolve({ accessKeyId: 'a', secretAccessKey: 's' }) }), false], + [BaseCredentials.none(), false], +])('credentials %s respects environment variables: %p', async (baseCredentials, respectsEnv) => { + const toolkit = new Toolkit({ + ioHost, + sdkConfig: { baseCredentials }, + }); + + // We create the SdkProvider currently *inside* `appFixture`, so this needs to be set beforehand + process.env.AWS_REGION = 'south-pole-1'; + + const cx = await appFixture(toolkit, 'stack-with-env-from-env'); + await using _ = await toolkit.synth(cx); + + expect(makeSdk).toHaveBeenCalledWith(expect.anything(), respectsEnv ? 'south-pole-1' : 'us-east-1'); + + delete process.env.AWS_REGION; +}); diff --git a/packages/@aws-cdk/toolkit-lib/test/toolkit/plugins.test.ts b/packages/@aws-cdk/toolkit-lib/test/toolkit/plugins.test.ts new file mode 100644 index 000000000..c6d797fa7 --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/toolkit/plugins.test.ts @@ -0,0 +1,60 @@ +import type { PluginProviderResult } from '@aws-cdk/cli-plugin-contract'; +import { StackSelectionStrategy } from '../../lib'; +import { SdkProvider } from '../../lib/api/shared-private'; +import { Toolkit } from '../../lib/toolkit/toolkit'; +import { appFixture, TestIoHost } from '../_helpers'; +import { MockSdk } from '../_helpers/mock-sdk'; + +test('two toolkit instances have independent plugin hosts by default', () => { + // GIVEN + const toolkit1 = new Toolkit(); + const toolkit2 = new Toolkit(); + + // WHEN + toolkit1.pluginHost.registerCredentialProviderSource({ + name: 'test', + isAvailable: () => Promise.resolve(false), + canProvideCredentials: () => Promise.resolve(false), + getProvider: () => Promise.reject('should not be called'), + }); + + // THEN + expect(toolkit1.pluginHost.credentialProviderSources.length).toEqual(1); + expect(toolkit2.pluginHost.credentialProviderSources.length).toEqual(0); +}); + +test('credential plugins registered into toolkit are queried', async () => { + // GIVEN + const toolkit = new Toolkit({ + ioHost: new TestIoHost(), + }); + + const canProvideCredentials = jest.fn().mockResolvedValue(true); + const getProvider = jest.fn().mockResolvedValue({ + accessKeyId: 'a', + secretAccessKey: 's', + } satisfies PluginProviderResult); + + toolkit.pluginHost.registerCredentialProviderSource({ + name: 'test plugin', + isAvailable: () => Promise.resolve(true), + canProvideCredentials, + getProvider, + }); + + const mockSdk = jest.spyOn(SdkProvider.prototype, '_makeSdk').mockReturnValue(new MockSdk()); + + // WHEN - simplest API that uses some credentials + const cx = await appFixture(toolkit, 'stack-with-defined-env'); + // We expect this to fail because we didn't really put in the effort to make the mocks return + // something sensible. + await expect(toolkit.rollback(cx, { + stacks: { strategy: StackSelectionStrategy.ALL_STACKS }, + })).rejects.toThrow(); + + // THEN + expect(canProvideCredentials).toHaveBeenCalled(); + expect(getProvider).toHaveBeenCalledWith('11111111111', 0, expect.anything()); + + mockSdk.mockRestore(); +}); diff --git a/packages/@aws-cdk/toolkit-lib/test/tsconfig.json b/packages/@aws-cdk/toolkit-lib/test/tsconfig.json new file mode 100644 index 000000000..bf74a099c --- /dev/null +++ b/packages/@aws-cdk/toolkit-lib/test/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../tsconfig.dev.json", + "compilerOptions": { + "rootDir": "..", + "noEmit": true + }, + "references": [ + { + "path": "../../cloud-assembly-schema" + }, + { + "path": "../../cloudformation-diff" + }, + { + "path": "../../../cdk-assets" + }, + { + "path": "../../tmp-toolkit-helpers" + } + ], + "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." +} diff --git a/packages/@aws-cdk/user-input-gen/tsconfig.dev.json b/packages/@aws-cdk/user-input-gen/tsconfig.dev.json index c23fc6c66..5bfbfa299 100644 --- a/packages/@aws-cdk/user-input-gen/tsconfig.dev.json +++ b/packages/@aws-cdk/user-input-gen/tsconfig.dev.json @@ -8,8 +8,7 @@ "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2020", - "dom" + "es2020" ], "module": "commonjs", "noEmitOnError": false, diff --git a/packages/@aws-cdk/user-input-gen/tsconfig.json b/packages/@aws-cdk/user-input-gen/tsconfig.json index 494d42c75..4fe81a0dd 100644 --- a/packages/@aws-cdk/user-input-gen/tsconfig.json +++ b/packages/@aws-cdk/user-input-gen/tsconfig.json @@ -10,8 +10,7 @@ "inlineSourceMap": true, "inlineSources": true, "lib": [ - "es2020", - "dom" + "es2020" ], "module": "commonjs", "noEmitOnError": false, diff --git a/packages/aws-cdk/.gitattributes b/packages/aws-cdk/.gitattributes index c1b26c9d0..f8b7d2522 100644 --- a/packages/aws-cdk/.gitattributes +++ b/packages/aws-cdk/.gitattributes @@ -15,6 +15,7 @@ /jest.config.json linguist-generated /LICENSE linguist-generated /package.json linguist-generated +/test/tsconfig.json linguist-generated /tsconfig.dev.json linguist-generated /tsconfig.json linguist-generated /yarn.lock linguist-generated \ No newline at end of file diff --git a/packages/aws-cdk/.gitignore b/packages/aws-cdk/.gitignore index 53b4f9b53..f63afe223 100644 --- a/packages/aws-cdk/.gitignore +++ b/packages/aws-cdk/.gitignore @@ -44,6 +44,7 @@ jspm_packages/ /dist/changelog.md /dist/version.txt !/.eslintrc.js +!/test/tsconfig.json db.json.gz .init-version.json index_bg.wasm diff --git a/packages/aws-cdk/.projen/files.json b/packages/aws-cdk/.projen/files.json index 493bbd87e..9b2047f9d 100644 --- a/packages/aws-cdk/.projen/files.json +++ b/packages/aws-cdk/.projen/files.json @@ -12,6 +12,7 @@ ".projen/tasks.json", "jest.config.json", "LICENSE", + "test/tsconfig.json", "tsconfig.dev.json", "tsconfig.json" ], diff --git a/packages/aws-cdk/.projen/tasks.json b/packages/aws-cdk/.projen/tasks.json index daa8cae78..57bc5e088 100644 --- a/packages/aws-cdk/.projen/tasks.json +++ b/packages/aws-cdk/.projen/tasks.json @@ -61,7 +61,7 @@ "name": "check-licenses", "steps": [ { - "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC\"", + "exec": "license-checker --summary --production --onlyAllow \"Apache-2.0;MIT;ISC;BSD-3-Clause\"", "receiveArgs": true } ] @@ -73,6 +73,9 @@ { "exec": "tsc --build", "receiveArgs": true + }, + { + "exec": "tsc --build test" } ] }, diff --git a/packages/aws-cdk/THIRD_PARTY_LICENSES b/packages/aws-cdk/THIRD_PARTY_LICENSES index 070ca1a38..601628b54 100644 --- a/packages/aws-cdk/THIRD_PARTY_LICENSES +++ b/packages/aws-cdk/THIRD_PARTY_LICENSES @@ -2266,7 +2266,7 @@ The aws-cdk package includes the following third-party software/licensing: ---------------- -** @aws-sdk/client-ecs@3.787.0 - https://www.npmjs.com/package/@aws-sdk/client-ecs/v/3.787.0 | Apache-2.0 +** @aws-sdk/client-ecs@3.791.0 - https://www.npmjs.com/package/@aws-sdk/client-ecs/v/3.791.0 | Apache-2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index adb6db1fd..fd15044af 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -19,7 +19,6 @@ import type { DeploymentMethod } from '../api/deployments'; import { Deployments } from '../api/deployments'; import { HotswapMode } from '../api/hotswap'; import { Notices } from '../api/notices'; -import { PluginHost } from '../api/plugin'; import type { IReadLock } from '../api/rwlock'; import type { Settings } from '../api/settings'; import { ToolkitInfo } from '../api/toolkit-info'; @@ -30,6 +29,8 @@ import { cliInit, printAvailableTemplates } from '../commands/init'; import { getMigrateScanType } from '../commands/migrate'; import { execProgram, CloudExecutable } from '../cxapp'; import type { StackSelector, Synthesizer } from '../cxapp'; +import { GLOBAL_PLUGIN_HOST } from './singleton-plugin-host'; +import { makeRequestHandler } from '../../../@aws-cdk/toolkit-lib/lib/api/shared-private'; /* eslint-disable max-len */ /* eslint-disable @typescript-eslint/no-shadow */ // yargs @@ -121,11 +122,12 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise(); for (const source of settings) { const plugins: string[] = source.get(['plugin']) || []; for (const plugin of plugins) { - const resolved = tryResolve(plugin); - if (loaded.has(resolved)) { - continue; - } - ioHost.defaults.debug(`Loading plug-in: ${chalk.green(plugin)} from ${chalk.blue(resolved)}`); - PluginHost.instance.load(plugin); - loaded.add(resolved); - } - } - - function tryResolve(plugin: string): string { - try { - return require.resolve(plugin); - } catch (e: any) { - // according to Node.js docs `MODULE_NOT_FOUND` is the only possible error here - // @see https://nodejs.org/api/modules.html#requireresolverequest-options - // Not using `withCause()` here, since the node error contains a "Require Stack" - // as part of the error message that is inherently useless to our users. - throw new ToolkitError(`Unable to resolve plug-in: Cannot find module '${plugin}'`); + GLOBAL_PLUGIN_HOST.load(plugin, ioHost); } } } diff --git a/packages/aws-cdk/lib/cli/singleton-plugin-host.ts b/packages/aws-cdk/lib/cli/singleton-plugin-host.ts new file mode 100644 index 000000000..51f103960 --- /dev/null +++ b/packages/aws-cdk/lib/cli/singleton-plugin-host.ts @@ -0,0 +1,9 @@ +/** + * The singleton plugin host + * + * This is only a concept in the CLI, not in the toolkit library. + */ + +import { PluginHost } from '../../../@aws-cdk/tmp-toolkit-helpers'; + +export const GLOBAL_PLUGIN_HOST = new PluginHost(); diff --git a/packages/aws-cdk/lib/cxapp/cloud-executable.ts b/packages/aws-cdk/lib/cxapp/cloud-executable.ts index 8c7608a8e..500660381 100644 --- a/packages/aws-cdk/lib/cxapp/cloud-executable.ts +++ b/packages/aws-cdk/lib/cxapp/cloud-executable.ts @@ -5,6 +5,7 @@ import { BorrowedAssembly } from '../../../@aws-cdk/toolkit-lib/lib/api/cloud-as import { ToolkitError } from '../api'; import type { SdkProvider } from '../api/aws-auth'; import { IO, type IoHelper } from '../api-private'; +import { GLOBAL_PLUGIN_HOST } from '../cli/singleton-plugin-host'; import type { Configuration } from '../cli/user-configuration'; import * as contextproviders from '../context-providers'; @@ -109,6 +110,7 @@ export class CloudExecutable implements ICloudAssemblySource { assembly.manifest.missing, this.props.configuration.context, this.props.sdkProvider, + GLOBAL_PLUGIN_HOST, this.props.ioHelper, ); diff --git a/packages/aws-cdk/lib/init-templates/.init-version.json b/packages/aws-cdk/lib/init-templates/.init-version.json index ed97beb3d..847316e85 100644 --- a/packages/aws-cdk/lib/init-templates/.init-version.json +++ b/packages/aws-cdk/lib/init-templates/.init-version.json @@ -1 +1 @@ -{"aws-cdk-lib": "2.189.0", "constructs": "^10.0.0"} +{"aws-cdk-lib": "2.190.0", "constructs": "^10.0.0"} diff --git a/packages/aws-cdk/lib/init-templates/.recommended-feature-flags.json b/packages/aws-cdk/lib/init-templates/.recommended-feature-flags.json index 585c8534d..3b6f870cb 100644 --- a/packages/aws-cdk/lib/init-templates/.recommended-feature-flags.json +++ b/packages/aws-cdk/lib/init-templates/.recommended-feature-flags.json @@ -68,5 +68,7 @@ "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, "@aws-cdk/aws-events:requireEventBusPolicySid": true, - "@aws-cdk/aws-dynamodb:retainTableReplica": true + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true } \ No newline at end of file diff --git a/packages/aws-cdk/lib/init-templates/app/typescript/tsconfig.json b/packages/aws-cdk/lib/init-templates/app/typescript/tsconfig.json index aaa7dc510..fc44377a1 100644 --- a/packages/aws-cdk/lib/init-templates/app/typescript/tsconfig.json +++ b/packages/aws-cdk/lib/init-templates/app/typescript/tsconfig.json @@ -3,8 +3,7 @@ "target": "ES2020", "module": "commonjs", "lib": [ - "es2020", - "dom" + "es2020" ], "declaration": true, "strict": true, diff --git a/packages/aws-cdk/lib/init-templates/lib/typescript/tsconfig.json b/packages/aws-cdk/lib/init-templates/lib/typescript/tsconfig.json index 1fa2eb4d6..8ffead765 100644 --- a/packages/aws-cdk/lib/init-templates/lib/typescript/tsconfig.json +++ b/packages/aws-cdk/lib/init-templates/lib/typescript/tsconfig.json @@ -3,8 +3,7 @@ "target": "ES2020", "module": "commonjs", "lib": [ - "es2020", - "dom" + "es2020" ], "declaration": true, "strict": true, diff --git a/packages/aws-cdk/lib/init-templates/sample-app/javascript/tsconfig.json b/packages/aws-cdk/lib/init-templates/sample-app/javascript/tsconfig.json index 2d5ebd700..0662466bf 100644 --- a/packages/aws-cdk/lib/init-templates/sample-app/javascript/tsconfig.json +++ b/packages/aws-cdk/lib/init-templates/sample-app/javascript/tsconfig.json @@ -3,8 +3,7 @@ "target": "ES2020", "module": "commonjs", "lib": [ - "es2020", - "dom" + "es2020" ], "declaration": true, "strict": true, diff --git a/packages/aws-cdk/lib/init-templates/sample-app/typescript/tsconfig.json b/packages/aws-cdk/lib/init-templates/sample-app/typescript/tsconfig.json index aaa7dc510..fc44377a1 100644 --- a/packages/aws-cdk/lib/init-templates/sample-app/typescript/tsconfig.json +++ b/packages/aws-cdk/lib/init-templates/sample-app/typescript/tsconfig.json @@ -3,8 +3,7 @@ "target": "ES2020", "module": "commonjs", "lib": [ - "es2020", - "dom" + "es2020" ], "declaration": true, "strict": true, diff --git a/packages/aws-cdk/lib/legacy-aws-auth.ts b/packages/aws-cdk/lib/legacy-aws-auth.ts index dfaf5df1e..218cafae3 100644 --- a/packages/aws-cdk/lib/legacy-aws-auth.ts +++ b/packages/aws-cdk/lib/legacy-aws-auth.ts @@ -9,6 +9,7 @@ import type { AwsCredentialIdentityProvider, Logger, NodeHttpHandlerOptions } from '@smithy/types'; import { SdkProvider as SdkProviderCurrentVersion } from './api/aws-auth'; import { CliIoHost } from './cli/io-host'; +import { GLOBAL_PLUGIN_HOST } from './cli/singleton-plugin-host'; /** * @deprecated @@ -96,6 +97,7 @@ export class SdkProvider { return SdkProviderCurrentVersion.withAwsCliCompatibleDefaults({ ...options, ioHelper: CliIoHost.instance().asIoHelper(), + pluginHost: GLOBAL_PLUGIN_HOST, }); } @@ -105,6 +107,11 @@ export class SdkProvider { requestHandler: NodeHttpHandlerOptions = {}, logger?: Logger, ) { - return new SdkProviderCurrentVersion(defaultCredentialProvider, defaultRegion, requestHandler, CliIoHost.instance().asIoHelper(), logger); + return new SdkProviderCurrentVersion(defaultCredentialProvider, defaultRegion, { + pluginHost: GLOBAL_PLUGIN_HOST, + ioHelper: CliIoHost.instance().asIoHelper(), + requestHandler, + logger, + }); } } diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 5bb3a4dbe..b0319d0cc 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -53,7 +53,7 @@ "@types/yargs": "^15", "@typescript-eslint/eslint-plugin": "^8", "@typescript-eslint/parser": "^8", - "aws-cdk-lib": "^2.189.0", + "aws-cdk-lib": "^2.190.0", "axios": "^1.8.4", "commit-and-tag-version": "^12", "constructs": "^10.0.0", @@ -71,7 +71,7 @@ "jest-mock": "^29.7.0", "license-checker": "^25.0.1", "madge": "^8.0.0", - "nock": "^14.0.3", + "nock": "^14.0.4", "prettier": "^2.8", "projen": "^0.91.20", "sinon": "^19.0.5", @@ -83,8 +83,8 @@ "dependencies": { "@aws-cdk/cloud-assembly-schema": "^0.0.0", "@aws-cdk/cloudformation-diff": "^0.0.0", - "@aws-cdk/cx-api": "^2.189.0", - "@aws-cdk/region-info": "^2.189.0", + "@aws-cdk/cx-api": "^2.190.0", + "@aws-cdk/region-info": "^2.190.0", "@aws-sdk/client-appsync": "^3", "@aws-sdk/client-cloudcontrol": "^3", "@aws-sdk/client-cloudformation": "^3", diff --git a/packages/aws-cdk/test/_helpers/mock-sdk.ts b/packages/aws-cdk/test/_helpers/mock-sdk.ts index e08c25292..f4f620166 100644 --- a/packages/aws-cdk/test/_helpers/mock-sdk.ts +++ b/packages/aws-cdk/test/_helpers/mock-sdk.ts @@ -146,7 +146,9 @@ export class MockSdkProvider extends SdkProvider { private defaultAccounts: string[] = []; constructor() { - super(FAKE_CREDENTIAL_CHAIN, 'bermuda-triangle-1337', {}, new TestIoHost().asHelper('sdk')); + super(FAKE_CREDENTIAL_CHAIN, 'bermuda-triangle-1337', { + ioHelper: new TestIoHost().asHelper('sdk'), + }); } public returnsDefaultAccounts(...accounts: string[]) { @@ -156,7 +158,7 @@ export class MockSdkProvider extends SdkProvider { public defaultAccount(): Promise { const accountId = this.defaultAccounts.length === 0 ? '123456789012' - : this.defaultAccounts.shift(); + : this.defaultAccounts.shift()!; return Promise.resolve({ accountId, partition: 'aws' }); } } diff --git a/packages/aws-cdk/test/api/aws-auth/awscli-compatible.test.ts b/packages/aws-cdk/test/api/aws-auth/awscli-compatible.test.ts index 50680e336..d83be5424 100644 --- a/packages/aws-cdk/test/api/aws-auth/awscli-compatible.test.ts +++ b/packages/aws-cdk/test/api/aws-auth/awscli-compatible.test.ts @@ -226,7 +226,7 @@ async function region(opts: { process.env.AWS_SHARED_CREDENTIALS_FILE = credentialsPath; } - return await new AwsCliCompatible(ioHelper).region(opts.profile); + return await new AwsCliCompatible(ioHelper, {}).region(opts.profile); } finally { fs.removeSync(workdir); } @@ -243,7 +243,7 @@ describe('Session token', () => { delete process.env.AWS_SESSION_TOKEN; delete process.env.AMAZON_SESSION_TOKEN; - await new AwsCliCompatible(ioHelper).credentialChainBuilder(); + await new AwsCliCompatible(ioHelper, {}).credentialChainBuilder(); expect(process.env.AWS_SESSION_TOKEN).toBeUndefined(); }); @@ -252,7 +252,7 @@ describe('Session token', () => { process.env.AWS_SESSION_TOKEN = 'aaa'; delete process.env.AMAZON_SESSION_TOKEN; - await new AwsCliCompatible(ioHelper).credentialChainBuilder(); + await new AwsCliCompatible(ioHelper, {}).credentialChainBuilder(); expect(process.env.AWS_SESSION_TOKEN).toEqual('aaa'); }); @@ -261,7 +261,7 @@ describe('Session token', () => { delete process.env.AWS_SESSION_TOKEN; process.env.AMAZON_SESSION_TOKEN = 'aaa'; - await new AwsCliCompatible(ioHelper).credentialChainBuilder(); + await new AwsCliCompatible(ioHelper, {}).credentialChainBuilder(); expect(process.env.AWS_SESSION_TOKEN).toEqual('aaa'); }); @@ -270,7 +270,7 @@ describe('Session token', () => { process.env.AWS_SESSION_TOKEN = 'aaa'; process.env.AMAZON_SESSION_TOKEN = 'bbb'; - await new AwsCliCompatible(ioHelper).credentialChainBuilder(); + await new AwsCliCompatible(ioHelper, {}).credentialChainBuilder(); expect(process.env.AWS_SESSION_TOKEN).toEqual('aaa'); }); diff --git a/packages/aws-cdk/test/api/aws-auth/credential-plugins.test.ts b/packages/aws-cdk/test/api/aws-auth/credential-plugins.test.ts index da90ffde8..bb5a76139 100644 --- a/packages/aws-cdk/test/api/aws-auth/credential-plugins.test.ts +++ b/packages/aws-cdk/test/api/aws-auth/credential-plugins.test.ts @@ -1,8 +1,8 @@ import type { PluginProviderResult, SDKv2CompatibleCredentials } from '@aws-cdk/cli-plugin-contract'; import { CredentialPlugins } from '../../../lib/api/aws-auth'; -import { PluginHost } from '../../../lib/api/plugin'; import { Mode } from '../../../lib/api/plugin'; import { TestIoHost } from '../../_helpers/io-host'; +import { GLOBAL_PLUGIN_HOST } from '../../../lib/cli/singleton-plugin-host'; const ioHost = new TestIoHost(); const ioHelper = ioHost.asHelper('deploy'); @@ -14,7 +14,7 @@ test('returns credential from plugin', async () => { secretAccessKey: 'bbb', getPromise: () => Promise.resolve(), } satisfies SDKv2CompatibleCredentials; - const host = PluginHost.instance; + const host = GLOBAL_PLUGIN_HOST; host.registerCredentialProviderSource({ name: 'Fake', diff --git a/packages/aws-cdk/test/api/aws-auth/sdk-logger.test.ts b/packages/aws-cdk/test/api/aws-auth/sdk-logger.test.ts index 0ab033baf..c3593bc07 100644 --- a/packages/aws-cdk/test/api/aws-auth/sdk-logger.test.ts +++ b/packages/aws-cdk/test/api/aws-auth/sdk-logger.test.ts @@ -5,7 +5,7 @@ describe(SdkToCliLogger, () => { notify: jest.fn(), requestResponse: jest.fn(), }; - const logger = new SdkToCliLogger(ioHost); + const logger = new SdkToCliLogger(ioHost as any); beforeEach(() => { ioHost.notify.mockReset(); diff --git a/packages/aws-cdk/test/api/aws-auth/sdk-provider.test.ts b/packages/aws-cdk/test/api/aws-auth/sdk-provider.test.ts index daa6b11e8..35ce1c462 100644 --- a/packages/aws-cdk/test/api/aws-auth/sdk-provider.test.ts +++ b/packages/aws-cdk/test/api/aws-auth/sdk-provider.test.ts @@ -22,11 +22,11 @@ import { FakeSts, RegisterRoleOptions, RegisterUserOptions } from './fake-sts'; import { ConfigurationOptions, CredentialsOptions, SDK, SdkProvider } from '../../../lib/api/aws-auth'; import { AwsCliCompatible } from '../../../lib/api/aws-auth'; import { defaultCliUserAgent } from '../../../lib/api/aws-auth'; -import { PluginHost } from '../../../lib/api/plugin'; import { Mode } from '../../../lib/api/plugin'; -import { instanceMockFrom, withMocked } from '../../_helpers/as-mock'; +import { withMocked } from '../../_helpers/as-mock'; import { undoAllSdkMocks } from '../../_helpers/mock-sdk'; import { TestIoHost } from '../../_helpers/io-host'; +import { GLOBAL_PLUGIN_HOST } from '../../../lib/cli/singleton-plugin-host'; // As part of the imports above we import `mock-sdk.ts` which automatically mocks // all SDK clients. We don't want that for this test suite, so undo it. @@ -67,8 +67,8 @@ beforeEach(() => { ioHost.notifySpy.mockClear(); ioHost.requestSpy.mockClear(); - PluginHost.instance.credentialProviderSources.splice(0); - PluginHost.instance.credentialProviderSources.push({ + GLOBAL_PLUGIN_HOST.credentialProviderSources.splice(0); + GLOBAL_PLUGIN_HOST.credentialProviderSources.push({ isAvailable() { return Promise.resolve(true); }, @@ -163,7 +163,7 @@ describe('with intercepted network calls', () => { const error = new Error('Expired Token'); error.name = 'ExpiredToken'; const identityProvider = () => Promise.reject(error); - const provider = new SdkProvider(identityProvider, 'rgn', {}, ioHelper); + const provider = new SdkProvider(identityProvider, 'rgn', { ioHelper, pluginHost: GLOBAL_PLUGIN_HOST }); const creds = await provider.baseCredentialsPartition({ ...env(account), region: 'rgn' }, Mode.ForReading); expect(creds).toBeUndefined(); @@ -325,7 +325,7 @@ describe('with intercepted network calls', () => { // The profile is not passed explicitly. Should be picked from the environment variable process.env.AWS_PROFILE = 'mfa-role'; // Awaiting to make sure the environment variable is only deleted after it's used - const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ioHelper, logger: console }); + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ioHelper, logger: console, pluginHost: GLOBAL_PLUGIN_HOST }); delete process.env.AWS_PROFILE; return Promise.resolve(provider); }), @@ -829,7 +829,7 @@ function isProfileRole(x: ProfileUser | ProfileRole): x is ProfileRole { } async function providerFromProfile(profile: string | undefined) { - return SdkProvider.withAwsCliCompatibleDefaults({ ioHelper, profile, logger: console }); + return SdkProvider.withAwsCliCompatibleDefaults({ ioHelper, profile, logger: console, pluginHost: GLOBAL_PLUGIN_HOST }); } async function exerciseCredentials(provider: SdkProvider, e: cxapi.Environment, mode: Mode = Mode.ForReading, diff --git a/packages/aws-cdk/test/api/cloudformation/evaluate-cloudformation-template.test.ts b/packages/aws-cdk/test/api/cloudformation/evaluate-cloudformation-template.test.ts index 59586d8d4..3496c99f1 100644 --- a/packages/aws-cdk/test/api/cloudformation/evaluate-cloudformation-template.test.ts +++ b/packages/aws-cdk/test/api/cloudformation/evaluate-cloudformation-template.test.ts @@ -17,6 +17,7 @@ const createEvaluateCloudFormationTemplate = (template: Template) => region: 'ap-south-east-2', partition: 'aws', sdk, + stackArtifact: {} as any, }); describe('evaluateCfnExpression', () => { diff --git a/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts b/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts index 280f52330..e74f1b776 100644 --- a/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts +++ b/packages/aws-cdk/test/api/hotswap/appsync-mapping-templates-hotswap-deployments.test.ts @@ -135,7 +135,7 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot // GIVEN const body = getBodyStream('template defined in s3'); mockS3Client.on(GetObjectCommand).resolves({ - Body: body, + Body: body as any, }); setup.setCurrentCfnStackTemplate({ Resources: { @@ -212,7 +212,7 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot // GIVEN const body = getBodyStream('code defined in s3'); mockS3Client.on(GetObjectCommand).resolves({ - Body: body, + Body: body as any, }); setup.setCurrentCfnStackTemplate({ Resources: { @@ -731,7 +731,7 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot async () => { // GIVEN mockS3Client.on(GetObjectCommand).resolves({ - Body: getBodyStream('template defined in s3'), + Body: getBodyStream('template defined in s3') as any, }); mockAppSyncClient .on(ListFunctionsCommand) @@ -1269,7 +1269,7 @@ describe.each([HotswapMode.FALL_BACK, HotswapMode.HOTSWAP_ONLY])('%p mode', (hot async () => { // GIVEN mockS3Client.on(GetObjectCommand).resolves({ - Body: getBodyStream('schema defined in s3'), + Body: getBodyStream('schema defined in s3') as any, }); setup.setCurrentCfnStackTemplate({ Resources: { diff --git a/packages/aws-cdk/test/api/plugin/credential-plugin.test.ts b/packages/aws-cdk/test/api/plugin/credential-plugin.test.ts index 003e32d3a..07d06649b 100644 --- a/packages/aws-cdk/test/api/plugin/credential-plugin.test.ts +++ b/packages/aws-cdk/test/api/plugin/credential-plugin.test.ts @@ -170,7 +170,7 @@ function mockCredentialPlugin(p: CredentialProviderSource) { }; }, { virtual: true }); - host.load(THE_PLUGIN); + host._doLoad(THE_PLUGIN); } async function fetchNow() { diff --git a/packages/aws-cdk/test/api/plugin/plugin-host.test.ts b/packages/aws-cdk/test/api/plugin/plugin-host.test.ts index 6e990606e..01962c052 100644 --- a/packages/aws-cdk/test/api/plugin/plugin-host.test.ts +++ b/packages/aws-cdk/test/api/plugin/plugin-host.test.ts @@ -20,7 +20,7 @@ test('load a plugin using the PluginHost', () => { }; }, { virtual: true }); - host.load(THE_PLUGIN); + host._doLoad(THE_PLUGIN); }); test('fail to load a plugin using the PluginHost', () => { @@ -31,7 +31,7 @@ test('fail to load a plugin using the PluginHost', () => { return {}; }, { virtual: true }); - expect(() => host.load(THE_PLUGIN)).toThrow(/Unable to load plug-in/); + expect(() => host._doLoad(THE_PLUGIN)).toThrow(/Unable to load plug-in/); }); test('plugin that registers a Credential Provider', () => { @@ -52,7 +52,7 @@ test('plugin that registers a Credential Provider', () => { }; }, { virtual: true }); - host.load(THE_PLUGIN); + host._doLoad(THE_PLUGIN); expect(host.credentialProviderSources).toHaveLength(1); }); @@ -73,7 +73,7 @@ test('plugin that registers a Context Provider', () => { }; }, { virtual: true }); - host.load(THE_PLUGIN); + host._doLoad(THE_PLUGIN); expect(Object.keys(host.contextProviderPlugins)).toHaveLength(1); }); @@ -91,9 +91,9 @@ test('plugin that registers an invalid Context Provider throws', () => { }, { virtual: true }); try { - host.load(THE_PLUGIN); + host._doLoad(THE_PLUGIN); expect(true).toBe(false); // should not happen - } catch(e) { + } catch(e: any) { expect(e).toHaveProperty('cause'); expect(e.cause?.message).toMatch(/does not look like a ContextProviderPlugin/); } diff --git a/packages/aws-cdk/test/api/work-graph/work-graph-builder.test.ts b/packages/aws-cdk/test/api/work-graph/work-graph-builder.test.ts index 05c7b11c3..a9651446e 100644 --- a/packages/aws-cdk/test/api/work-graph/work-graph-builder.test.ts +++ b/packages/aws-cdk/test/api/work-graph/work-graph-builder.test.ts @@ -7,13 +7,11 @@ import { CloudAssemblyBuilder } from '@aws-cdk/cx-api'; import { expect } from '@jest/globals'; import { WorkGraph, WorkGraphBuilder } from '../../../lib/api/work-graph'; import type { AssetBuildNode, AssetPublishNode, StackNode, WorkNode } from '../../../lib/api/work-graph'; -import { CliIoHost, IoMessaging } from '../../../lib/cli/io-host'; +import { CliIoHost } from '../../../lib/cli/io-host'; +import { IoHelper } from '../../../lib/api-private'; let rootBuilder: CloudAssemblyBuilder; -let mockMsg: IoMessaging = { - ioHost: CliIoHost.instance(), - action: 'deploy' -}; +let mockMsg = IoHelper.fromIoHost(CliIoHost.instance(), 'deploy'); beforeEach(() => { rootBuilder = new CloudAssemblyBuilder(); diff --git a/packages/aws-cdk/test/cli/cdk-toolkit.test.ts b/packages/aws-cdk/test/cli/cdk-toolkit.test.ts index e92e7504f..024900cd6 100644 --- a/packages/aws-cdk/test/cli/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cli/cdk-toolkit.test.ts @@ -99,6 +99,7 @@ import { import { asIoHelper } from '../../../@aws-cdk/tmp-toolkit-helpers/src/api/io/private'; import { StackActivityProgress } from '../../lib/commands/deploy'; import { Template } from '../../../@aws-cdk/tmp-toolkit-helpers/src/api'; +import { DestroyStackResult } from '@aws-cdk/tmp-toolkit-helpers/src/api/deployments/deploy-stack'; markTesting(); @@ -682,11 +683,11 @@ describe('deploy', () => { ], }); }); - + test('lookup role is used', async () => { // GIVEN mockSSMClient.on(GetParameterCommand).resolves({ Parameter: { Value: '6' } }); - + const cdkToolkit = new CdkToolkit({ cloudExecutable: mockCloudExecutable, configuration: mockCloudExecutable.configuration, @@ -696,13 +697,13 @@ describe('deploy', () => { ioHelper, }), }); - + // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); - + // THEN expect(mockSSMClient).toHaveReceivedCommandWith(GetParameterCommand, { Name: '/bootstrap/parameter', @@ -722,11 +723,11 @@ describe('deploy', () => { }, ); }); - + test('fallback to deploy role if bootstrap stack version is not valid', async () => { // GIVEN mockSSMClient.on(GetParameterCommand).resolves({ Parameter: { Value: '1' } }); - + const cdkToolkit = new CdkToolkit({ cloudExecutable: mockCloudExecutable, configuration: mockCloudExecutable.configuration, @@ -736,17 +737,17 @@ describe('deploy', () => { ioHelper, }), }); - + // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); - + // THEN expect(flatten(stderrMock.mock.calls)).toEqual( expect.arrayContaining([ - + expect.stringContaining( "Bootstrap stack version '5' is required, found version '1'. To get rid of this error, please upgrade to bootstrap version >= 5", ), @@ -783,7 +784,7 @@ describe('deploy', () => { }, ); }); - + test('fallback to deploy role if bootstrap version parameter not found', async () => { // GIVEN mockSSMClient.on(GetParameterCommand).callsFake(() => { @@ -791,7 +792,7 @@ describe('deploy', () => { e.code = e.name = 'ParameterNotFound'; throw e; }); - + const cdkToolkit = new CdkToolkit({ cloudExecutable: mockCloudExecutable, configuration: mockCloudExecutable.configuration, @@ -801,13 +802,13 @@ describe('deploy', () => { ioHelper, }), }); - + // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); - + // THEN expect(flatten(stderrMock.mock.calls)).toEqual( expect.arrayContaining([expect.stringMatching(/SSM parameter.*not found./)]), @@ -840,14 +841,14 @@ describe('deploy', () => { }, ); }); - + test('fallback to deploy role if forEnvironment throws', async () => { // GIVEN // throw error first for the 'prepareSdkWithLookupRoleFor' call and succeed for the rest mockForEnvironment = jest.spyOn(sdkProvider, 'forEnvironment').mockImplementationOnce(() => { throw new Error('TheErrorThatGetsThrown'); }); - + const cdkToolkit = new CdkToolkit({ cloudExecutable: mockCloudExecutable, configuration: mockCloudExecutable.configuration, @@ -857,13 +858,13 @@ describe('deploy', () => { ioHelper, }), }); - + // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); - + // THEN expect(mockSSMClient).not.toHaveReceivedAnyCommand(); expect(flatten(stderrMock.mock.calls)).toEqual( @@ -897,7 +898,7 @@ describe('deploy', () => { }, ); }); - + test('dont lookup bootstrap version parameter if default credentials are used', async () => { // GIVEN mockForEnvironment = jest.fn().mockImplementation(() => { @@ -913,13 +914,13 @@ describe('deploy', () => { ioHelper, }), }); - + // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-C'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); - + // THEN expect(flatten(stderrMock.mock.calls)).toEqual( expect.arrayContaining([ @@ -954,7 +955,7 @@ describe('deploy', () => { }, ); }); - + test('do not print warnings if lookup role not provided in stack artifact', async () => { // GIVEN const cdkToolkit = new CdkToolkit({ @@ -966,13 +967,13 @@ describe('deploy', () => { ioHelper, }), }); - + // WHEN await cdkToolkit.deploy({ selector: { patterns: ['Test-Stack-A'] }, hotswap: HotswapMode.FULL_DEPLOYMENT, }); - + // THEN expect(flatten(stderrMock.mock.calls)).not.toEqual( expect.arrayContaining([ @@ -1563,6 +1564,7 @@ describe('rollback', () => { const mockedRollback = jest.spyOn(Deployments.prototype, 'rollbackStack').mockResolvedValue({ success: true, + stackArn: 'arn', }); const toolkit = new CdkToolkit({ @@ -1602,7 +1604,7 @@ describe('rollback', () => { }); // Rollback might be called -- just don't do anything. - const mockRollbackStack = jest.spyOn(deployments, 'rollbackStack').mockResolvedValue({}); + const mockRollbackStack = jest.spyOn(deployments, 'rollbackStack').mockResolvedValue({ success: true, stackArn: 'arn' }); const mockedDeployStack = jest .spyOn(deployments, 'deployStack') @@ -1828,12 +1830,13 @@ class FakeCloudFormation extends Deployments { public rollbackStack(_options: RollbackStackOptions): Promise { return Promise.resolve({ success: true, - }); + stackArn: 'arn', + } satisfies RollbackStackResult); } - public destroyStack(options: DestroyStackOptions): Promise { + public destroyStack(options: DestroyStackOptions): Promise { expect(options.stack).toBeDefined(); - return Promise.resolve(); + return Promise.resolve({ stackArn: 'arn' }); } public readCurrentTemplate(stack: cxapi.CloudFormationStackArtifact): Promise