├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── auto-merge.yml │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ ├── upgrade-cdklabs-projen-project-types-main.yml │ ├── upgrade-dev-deps-main.yml │ └── upgrade-main.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── bin └── cdk-app ├── construct-commands.json ├── package.json ├── src ├── app.ts ├── cli.ts ├── command-store.ts ├── index.ts ├── matchers.ts ├── model.ts └── util.ts ├── test ├── fixtures │ ├── DemoAppStack.template.json │ └── tree.json └── matchers.test.ts ├── tsconfig.dev.json ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "project": "./tsconfig.dev.json" 17 | }, 18 | "extends": [ 19 | "plugin:import/typescript", 20 | "plugin:prettier/recommended" 21 | ], 22 | "settings": { 23 | "import/parsers": { 24 | "@typescript-eslint/parser": [ 25 | ".ts", 26 | ".tsx" 27 | ] 28 | }, 29 | "import/resolver": { 30 | "node": {}, 31 | "typescript": { 32 | "project": "./tsconfig.dev.json", 33 | "alwaysTryTypes": true 34 | } 35 | } 36 | }, 37 | "ignorePatterns": [ 38 | "*.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage", 43 | "!.projenrc.ts", 44 | "!projenrc/**/*.ts" 45 | ], 46 | "rules": { 47 | "@typescript-eslint/no-require-imports": [ 48 | "error" 49 | ], 50 | "import/no-extraneous-dependencies": [ 51 | "error", 52 | { 53 | "devDependencies": [ 54 | "**/test/**", 55 | "**/build-tools/**", 56 | ".projenrc.ts", 57 | "projenrc/**/*.ts" 58 | ], 59 | "optionalDependencies": false, 60 | "peerDependencies": true 61 | } 62 | ], 63 | "import/no-unresolved": [ 64 | "error" 65 | ], 66 | "import/order": [ 67 | "warn", 68 | { 69 | "groups": [ 70 | "builtin", 71 | "external" 72 | ], 73 | "alphabetize": { 74 | "order": "asc", 75 | "caseInsensitive": true 76 | } 77 | } 78 | ], 79 | "import/no-duplicates": [ 80 | "error" 81 | ], 82 | "no-shadow": [ 83 | "off" 84 | ], 85 | "@typescript-eslint/no-shadow": [ 86 | "error" 87 | ], 88 | "key-spacing": [ 89 | "error" 90 | ], 91 | "no-multiple-empty-lines": [ 92 | "error" 93 | ], 94 | "@typescript-eslint/no-floating-promises": [ 95 | "error" 96 | ], 97 | "no-return-await": [ 98 | "off" 99 | ], 100 | "@typescript-eslint/return-await": [ 101 | "error" 102 | ], 103 | "no-trailing-spaces": [ 104 | "error" 105 | ], 106 | "dot-notation": [ 107 | "error" 108 | ], 109 | "no-bitwise": [ 110 | "error" 111 | ], 112 | "@typescript-eslint/member-ordering": [ 113 | "error", 114 | { 115 | "default": [ 116 | "public-static-field", 117 | "public-static-method", 118 | "protected-static-field", 119 | "protected-static-method", 120 | "private-static-field", 121 | "private-static-method", 122 | "field", 123 | "constructor", 124 | "method" 125 | ] 126 | } 127 | ] 128 | }, 129 | "overrides": [ 130 | { 131 | "files": [ 132 | ".projenrc.ts" 133 | ], 134 | "rules": { 135 | "@typescript-eslint/no-require-imports": "off", 136 | "import/no-extraneous-dependencies": "off" 137 | } 138 | } 139 | ] 140 | } 141 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.eslintrc.json linguist-generated 6 | /.gitattributes linguist-generated 7 | /.github/pull_request_template.md linguist-generated 8 | /.github/workflows/auto-approve.yml linguist-generated 9 | /.github/workflows/auto-merge.yml linguist-generated 10 | /.github/workflows/build.yml linguist-generated 11 | /.github/workflows/pull-request-lint.yml linguist-generated 12 | /.github/workflows/release.yml linguist-generated 13 | /.github/workflows/upgrade-cdklabs-projen-project-types-main.yml linguist-generated 14 | /.github/workflows/upgrade-dev-deps-main.yml linguist-generated 15 | /.github/workflows/upgrade-main.yml linguist-generated 16 | /.gitignore linguist-generated 17 | /.npmignore linguist-generated 18 | /.prettierignore linguist-generated 19 | /.prettierrc.json linguist-generated 20 | /.projen/** linguist-generated 21 | /.projen/deps.json linguist-generated 22 | /.projen/files.json linguist-generated 23 | /.projen/tasks.json linguist-generated 24 | /LICENSE linguist-generated 25 | /package.json linguist-generated 26 | /tsconfig.dev.json linguist-generated 27 | /tsconfig.json linguist-generated 28 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: auto-approve 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | approve: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && (github.event.pull_request.user.login == 'cdklabs-automation' || github.event.pull_request.user.login == 'dependabot[bot]') 18 | steps: 19 | - uses: hmarr/auto-approve-action@v2.2.1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/auto-merge.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: auto-merge 4 | on: 5 | pull_request_target: 6 | types: 7 | - opened 8 | - reopened 9 | - ready_for_review 10 | jobs: 11 | enableAutoMerge: 12 | name: "Set AutoMerge on PR #${{ github.event.number }}" 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pull-requests: write 16 | contents: write 17 | steps: 18 | - uses: peter-evans/enable-pull-request-automerge@v2 19 | with: 20 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 21 | pull-request-number: ${{ github.event.number }} 22 | merge-method: squash 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | merge_group: 8 | branches: 9 | - main 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | outputs: 16 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 17 | env: 18 | CI: "true" 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | ref: ${{ github.event.pull_request.head.ref }} 24 | repository: ${{ github.event.pull_request.head.repo.full_name }} 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 20.x 29 | - name: Install dependencies 30 | run: yarn install --check-files 31 | - name: build 32 | run: npx projen build 33 | - name: Find mutations 34 | id: self_mutation 35 | run: |- 36 | git add . 37 | git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 38 | working-directory: ./ 39 | - name: Upload patch 40 | if: steps.self_mutation.outputs.self_mutation_happened 41 | uses: actions/upload-artifact@v4.4.0 42 | with: 43 | name: repo.patch 44 | path: repo.patch 45 | overwrite: true 46 | - name: Fail build on mutation 47 | if: steps.self_mutation.outputs.self_mutation_happened 48 | run: |- 49 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 50 | cat repo.patch 51 | exit 1 52 | self-mutation: 53 | needs: build 54 | runs-on: ubuntu-latest 55 | permissions: 56 | contents: write 57 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v4 61 | with: 62 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 63 | ref: ${{ github.event.pull_request.head.ref }} 64 | repository: ${{ github.event.pull_request.head.repo.full_name }} 65 | - name: Download patch 66 | uses: actions/download-artifact@v4 67 | with: 68 | name: repo.patch 69 | path: ${{ runner.temp }} 70 | - name: Apply patch 71 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 72 | - name: Set git identity 73 | run: |- 74 | git config user.name "github-actions" 75 | git config user.email "github-actions@github.com" 76 | - name: Push changes 77 | env: 78 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 79 | run: |- 80 | git add . 81 | git commit -s -m "chore: self mutation" 82 | git push origin HEAD:$PULL_REQUEST_REF 83 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | merge_group: 14 | branches: 15 | - main 16 | jobs: 17 | validate: 18 | name: Validate PR title 19 | runs-on: ubuntu-latest 20 | permissions: 21 | pull-requests: write 22 | steps: 23 | - uses: amannn/action-semantic-pull-request@v5.4.0 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | with: 27 | types: |- 28 | feat 29 | fix 30 | chore 31 | requireScope: false 32 | if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target' 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: false 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | outputs: 18 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 19 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 20 | env: 21 | CI: "true" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Set git identity 28 | run: |- 29 | git config user.name "github-actions" 30 | git config user.email "github-actions@github.com" 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: 20.x 35 | - name: Install dependencies 36 | run: yarn install --check-files --frozen-lockfile 37 | - name: release 38 | run: npx projen release 39 | - name: Check if version has already been tagged 40 | id: check_tag_exists 41 | run: |- 42 | TAG=$(cat dist/releasetag.txt) 43 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 44 | cat $GITHUB_OUTPUT 45 | - name: Check for new commits 46 | id: git_remote 47 | run: |- 48 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 49 | cat $GITHUB_OUTPUT 50 | - name: Backup artifact permissions 51 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 52 | run: cd dist && getfacl -R . > permissions-backup.acl 53 | continue-on-error: true 54 | - name: Upload artifact 55 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 56 | uses: actions/upload-artifact@v4.4.0 57 | with: 58 | name: build-artifact 59 | path: dist 60 | overwrite: true 61 | release_github: 62 | name: Publish to GitHub Releases 63 | needs: 64 | - release 65 | - release_npm 66 | runs-on: ubuntu-latest 67 | permissions: 68 | contents: write 69 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 70 | steps: 71 | - uses: actions/setup-node@v4 72 | with: 73 | node-version: 20.x 74 | - name: Download build artifacts 75 | uses: actions/download-artifact@v4 76 | with: 77 | name: build-artifact 78 | path: dist 79 | - name: Restore build artifact permissions 80 | run: cd dist && setfacl --restore=permissions-backup.acl 81 | continue-on-error: true 82 | - name: Release 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | GITHUB_REPOSITORY: ${{ github.repository }} 86 | GITHUB_REF: ${{ github.sha }} 87 | 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 88 | release_npm: 89 | name: Publish to npm 90 | needs: release 91 | runs-on: ubuntu-latest 92 | permissions: 93 | id-token: write 94 | contents: read 95 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 96 | steps: 97 | - uses: actions/setup-node@v4 98 | with: 99 | node-version: 20.x 100 | - name: Download build artifacts 101 | uses: actions/download-artifact@v4 102 | with: 103 | name: build-artifact 104 | path: dist 105 | - name: Restore build artifact permissions 106 | run: cd dist && setfacl --restore=permissions-backup.acl 107 | continue-on-error: true 108 | - name: Release 109 | env: 110 | NPM_DIST_TAG: latest 111 | NPM_REGISTRY: registry.npmjs.org 112 | NPM_CONFIG_PROVENANCE: "true" 113 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 114 | run: npx -p publib@latest publib-npm 115 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-cdklabs-projen-project-types-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-cdklabs-projen-project-types-main 4 | on: 5 | workflow_dispatch: {} 6 | jobs: 7 | upgrade: 8 | name: Upgrade 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | outputs: 13 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | ref: main 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v4 21 | - name: Install dependencies 22 | run: yarn install --check-files --frozen-lockfile 23 | - name: Upgrade dependencies 24 | run: npx projen upgrade-cdklabs-projen-project-types 25 | - name: Find mutations 26 | id: create_patch 27 | run: |- 28 | git add . 29 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 30 | working-directory: ./ 31 | - name: Upload patch 32 | if: steps.create_patch.outputs.patch_created 33 | uses: actions/upload-artifact@v4.4.0 34 | with: 35 | name: repo.patch 36 | path: repo.patch 37 | overwrite: true 38 | pr: 39 | name: Create Pull Request 40 | needs: upgrade 41 | runs-on: ubuntu-latest 42 | permissions: 43 | contents: read 44 | if: ${{ needs.upgrade.outputs.patch_created }} 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | with: 49 | ref: main 50 | - name: Download patch 51 | uses: actions/download-artifact@v4 52 | with: 53 | name: repo.patch 54 | path: ${{ runner.temp }} 55 | - name: Apply patch 56 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 57 | - name: Set git identity 58 | run: |- 59 | git config user.name "github-actions" 60 | git config user.email "github-actions@github.com" 61 | - name: Create Pull Request 62 | id: create-pr 63 | uses: peter-evans/create-pull-request@v6 64 | with: 65 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 66 | commit-message: |- 67 | chore(deps): upgrade cdklabs-projen-project-types 68 | 69 | Upgrades project dependencies. See details in [workflow run]. 70 | 71 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 72 | 73 | ------ 74 | 75 | *Automatically created by projen via the "upgrade-cdklabs-projen-project-types-main" workflow* 76 | branch: github-actions/upgrade-cdklabs-projen-project-types-main 77 | title: "chore(deps): upgrade cdklabs-projen-project-types" 78 | labels: auto-approve 79 | body: |- 80 | Upgrades project dependencies. See details in [workflow run]. 81 | 82 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 83 | 84 | ------ 85 | 86 | *Automatically created by projen via the "upgrade-cdklabs-projen-project-types-main" workflow* 87 | author: github-actions 88 | committer: github-actions 89 | signoff: true 90 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-dev-deps-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-dev-deps-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * * 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20.x 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade-dev-deps 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | chore(deps): upgrade dev dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-dev-deps-main" workflow* 80 | branch: github-actions/upgrade-dev-deps-main 81 | title: "chore(deps): upgrade dev dependencies" 82 | labels: auto-approve 83 | body: |- 84 | Upgrades project dependencies. See details in [workflow run]. 85 | 86 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 87 | 88 | ------ 89 | 90 | *Automatically created by projen via the "upgrade-dev-deps-main" workflow* 91 | author: github-actions 92 | committer: github-actions 93 | signoff: true 94 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 18 * * * 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20.x 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | fix(deps): upgrade dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-main" workflow* 80 | branch: github-actions/upgrade-main 81 | title: "fix(deps): upgrade dependencies" 82 | labels: auto-approve 83 | body: |- 84 | Upgrades project dependencies. See details in [workflow run]. 85 | 86 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 87 | 88 | ------ 89 | 90 | *Automatically created by projen via the "upgrade-main" workflow* 91 | author: github-actions 92 | committer: github-actions 93 | signoff: true 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/.github/workflows/auto-approve.yml 8 | !/package.json 9 | !/LICENSE 10 | !/.npmignore 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | lib-cov 23 | coverage 24 | *.lcov 25 | .nyc_output 26 | build/Release 27 | node_modules/ 28 | jspm_packages/ 29 | *.tsbuildinfo 30 | .eslintcache 31 | *.tgz 32 | .yarn-integrity 33 | .cache 34 | /test-reports/ 35 | junit.xml 36 | /coverage/ 37 | !/.github/workflows/build.yml 38 | /dist/changelog.md 39 | /dist/version.txt 40 | !/.github/workflows/release.yml 41 | !/.github/pull_request_template.md 42 | !/.prettierignore 43 | !/.prettierrc.json 44 | !/test/ 45 | !/tsconfig.json 46 | !/tsconfig.dev.json 47 | !/src/ 48 | /lib 49 | /dist/ 50 | !/.eslintrc.json 51 | !/.github/workflows/auto-merge.yml 52 | !/.github/workflows/upgrade-cdklabs-projen-project-types-main.yml 53 | !/.github/workflows/upgrade-main.yml 54 | !/.github/workflows/upgrade-dev-deps-main.yml 55 | !/.projenrc.ts 56 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /dist/changelog.md 8 | /dist/version.txt 9 | /.prettierignore 10 | /.prettierrc.json 11 | /test/ 12 | /tsconfig.dev.json 13 | /src/ 14 | !/lib/ 15 | !/lib/**/*.js 16 | !/lib/**/*.d.ts 17 | dist 18 | /tsconfig.json 19 | /.github/ 20 | /.vscode/ 21 | /.idea/ 22 | /.projenrc.js 23 | tsconfig.tsbuildinfo 24 | /.eslintrc.json 25 | /.gitattributes 26 | /.projenrc.ts 27 | /projenrc 28 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [] 3 | } 4 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@types/fs-extra", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@types/jest", 9 | "type": "build" 10 | }, 11 | { 12 | "name": "@types/node", 13 | "version": "^20", 14 | "type": "build" 15 | }, 16 | { 17 | "name": "@typescript-eslint/eslint-plugin", 18 | "version": "^7", 19 | "type": "build" 20 | }, 21 | { 22 | "name": "@typescript-eslint/parser", 23 | "version": "^7", 24 | "type": "build" 25 | }, 26 | { 27 | "name": "cdklabs-projen-project-types", 28 | "type": "build" 29 | }, 30 | { 31 | "name": "commit-and-tag-version", 32 | "version": "^12", 33 | "type": "build" 34 | }, 35 | { 36 | "name": "constructs", 37 | "version": "^10.0.0", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "eslint-config-prettier", 42 | "type": "build" 43 | }, 44 | { 45 | "name": "eslint-import-resolver-typescript", 46 | "type": "build" 47 | }, 48 | { 49 | "name": "eslint-plugin-import", 50 | "type": "build" 51 | }, 52 | { 53 | "name": "eslint-plugin-prettier", 54 | "type": "build" 55 | }, 56 | { 57 | "name": "eslint", 58 | "version": "^8", 59 | "type": "build" 60 | }, 61 | { 62 | "name": "jest", 63 | "type": "build" 64 | }, 65 | { 66 | "name": "jest-junit", 67 | "version": "^15", 68 | "type": "build" 69 | }, 70 | { 71 | "name": "prettier", 72 | "type": "build" 73 | }, 74 | { 75 | "name": "projen", 76 | "type": "build" 77 | }, 78 | { 79 | "name": "ts-jest", 80 | "type": "build" 81 | }, 82 | { 83 | "name": "ts-node", 84 | "type": "build" 85 | }, 86 | { 87 | "name": "typescript", 88 | "type": "build" 89 | }, 90 | { 91 | "name": "cdklabs-projen-project-types", 92 | "type": "runtime" 93 | }, 94 | { 95 | "name": "chalk", 96 | "version": "^4", 97 | "type": "runtime" 98 | }, 99 | { 100 | "name": "fs-extra", 101 | "type": "runtime" 102 | }, 103 | { 104 | "name": "open", 105 | "type": "runtime" 106 | }, 107 | { 108 | "name": "yaml", 109 | "type": "runtime" 110 | }, 111 | { 112 | "name": "yargs", 113 | "type": "runtime" 114 | } 115 | ], 116 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 117 | } 118 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/auto-approve.yml", 7 | ".github/workflows/auto-merge.yml", 8 | ".github/workflows/build.yml", 9 | ".github/workflows/pull-request-lint.yml", 10 | ".github/workflows/release.yml", 11 | ".github/workflows/upgrade-cdklabs-projen-project-types-main.yml", 12 | ".github/workflows/upgrade-dev-deps-main.yml", 13 | ".github/workflows/upgrade-main.yml", 14 | ".gitignore", 15 | ".npmignore", 16 | ".prettierignore", 17 | ".prettierrc.json", 18 | ".projen/deps.json", 19 | ".projen/files.json", 20 | ".projen/tasks.json", 21 | "LICENSE", 22 | "tsconfig.dev.json", 23 | "tsconfig.json" 24 | ], 25 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 26 | } 27 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "", 36 | "BUMP_PACKAGE": "commit-and-tag-version@^12", 37 | "RELEASABLE_COMMITS": "git log --no-merges --oneline $LATEST_TAG..HEAD -E --grep \"^(feat|fix){1}(\\([^()[:space:]]+\\))?(!)?:[[:blank:]]+.+\"" 38 | }, 39 | "steps": [ 40 | { 41 | "builtin": "release/bump-version" 42 | } 43 | ], 44 | "condition": "git log --oneline -1 | grep -qv \"chore(release):\"" 45 | }, 46 | "clobber": { 47 | "name": "clobber", 48 | "description": "hard resets to HEAD of origin and cleans the local repo", 49 | "env": { 50 | "BRANCH": "$(git branch --show-current)" 51 | }, 52 | "steps": [ 53 | { 54 | "exec": "git checkout -b scratch", 55 | "name": "save current HEAD in \"scratch\" branch" 56 | }, 57 | { 58 | "exec": "git checkout $BRANCH" 59 | }, 60 | { 61 | "exec": "git fetch origin", 62 | "name": "fetch latest changes from origin" 63 | }, 64 | { 65 | "exec": "git reset --hard origin/$BRANCH", 66 | "name": "hard reset to origin commit" 67 | }, 68 | { 69 | "exec": "git clean -fdx", 70 | "name": "clean all untracked files" 71 | }, 72 | { 73 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 74 | } 75 | ], 76 | "condition": "git diff --exit-code > /dev/null" 77 | }, 78 | "compile": { 79 | "name": "compile", 80 | "description": "Only compile", 81 | "steps": [ 82 | { 83 | "exec": "tsc --build" 84 | } 85 | ] 86 | }, 87 | "default": { 88 | "name": "default", 89 | "description": "Synthesize project files", 90 | "steps": [ 91 | { 92 | "exec": "ts-node --project tsconfig.dev.json .projenrc.ts" 93 | } 94 | ] 95 | }, 96 | "eject": { 97 | "name": "eject", 98 | "description": "Remove projen from the project", 99 | "env": { 100 | "PROJEN_EJECTING": "true" 101 | }, 102 | "steps": [ 103 | { 104 | "spawn": "default" 105 | } 106 | ] 107 | }, 108 | "eslint": { 109 | "name": "eslint", 110 | "description": "Runs eslint against the codebase", 111 | "steps": [ 112 | { 113 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ src test build-tools projenrc .projenrc.ts", 114 | "receiveArgs": true 115 | } 116 | ] 117 | }, 118 | "install": { 119 | "name": "install", 120 | "description": "Install project dependencies and update lockfile (non-frozen)", 121 | "steps": [ 122 | { 123 | "exec": "yarn install --check-files" 124 | } 125 | ] 126 | }, 127 | "install:ci": { 128 | "name": "install:ci", 129 | "description": "Install project dependencies using frozen lockfile", 130 | "steps": [ 131 | { 132 | "exec": "yarn install --check-files --frozen-lockfile" 133 | } 134 | ] 135 | }, 136 | "package": { 137 | "name": "package", 138 | "description": "Creates the distribution package", 139 | "steps": [ 140 | { 141 | "exec": "mkdir -p dist/js" 142 | }, 143 | { 144 | "exec": "npm pack --pack-destination dist/js" 145 | } 146 | ] 147 | }, 148 | "post-compile": { 149 | "name": "post-compile", 150 | "description": "Runs after successful compilation" 151 | }, 152 | "post-upgrade": { 153 | "name": "post-upgrade", 154 | "description": "Runs after upgrading dependencies" 155 | }, 156 | "pre-compile": { 157 | "name": "pre-compile", 158 | "description": "Prepare the project for compilation" 159 | }, 160 | "release": { 161 | "name": "release", 162 | "description": "Prepare a release from \"main\" branch", 163 | "env": { 164 | "RELEASE": "true" 165 | }, 166 | "steps": [ 167 | { 168 | "exec": "rm -fr dist" 169 | }, 170 | { 171 | "spawn": "bump" 172 | }, 173 | { 174 | "spawn": "build" 175 | }, 176 | { 177 | "spawn": "unbump" 178 | }, 179 | { 180 | "exec": "git diff --ignore-space-at-eol --exit-code" 181 | } 182 | ] 183 | }, 184 | "test": { 185 | "name": "test", 186 | "description": "Run tests", 187 | "steps": [ 188 | { 189 | "exec": "jest --passWithNoTests --updateSnapshot", 190 | "receiveArgs": true 191 | }, 192 | { 193 | "spawn": "eslint" 194 | } 195 | ] 196 | }, 197 | "test:watch": { 198 | "name": "test:watch", 199 | "description": "Run jest in watch mode", 200 | "steps": [ 201 | { 202 | "exec": "jest --watch" 203 | } 204 | ] 205 | }, 206 | "unbump": { 207 | "name": "unbump", 208 | "description": "Restores version to 0.0.0", 209 | "env": { 210 | "OUTFILE": "package.json", 211 | "CHANGELOG": "dist/changelog.md", 212 | "BUMPFILE": "dist/version.txt", 213 | "RELEASETAG": "dist/releasetag.txt", 214 | "RELEASE_TAG_PREFIX": "", 215 | "BUMP_PACKAGE": "commit-and-tag-version@^12", 216 | "RELEASABLE_COMMITS": "git log --no-merges --oneline $LATEST_TAG..HEAD -E --grep \"^(feat|fix){1}(\\([^()[:space:]]+\\))?(!)?:[[:blank:]]+.+\"" 217 | }, 218 | "steps": [ 219 | { 220 | "builtin": "release/reset-version" 221 | } 222 | ] 223 | }, 224 | "upgrade": { 225 | "name": "upgrade", 226 | "description": "upgrade dependencies", 227 | "env": { 228 | "CI": "0" 229 | }, 230 | "steps": [ 231 | { 232 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=prod --filter=fs-extra,open,yaml,yargs" 233 | }, 234 | { 235 | "exec": "yarn install --check-files" 236 | }, 237 | { 238 | "exec": "yarn upgrade chalk fs-extra open yaml yargs" 239 | }, 240 | { 241 | "exec": "npx projen" 242 | }, 243 | { 244 | "spawn": "post-upgrade" 245 | } 246 | ] 247 | }, 248 | "upgrade-cdklabs-projen-project-types": { 249 | "name": "upgrade-cdklabs-projen-project-types", 250 | "description": "upgrade cdklabs-projen-project-types", 251 | "env": { 252 | "CI": "0" 253 | }, 254 | "steps": [ 255 | { 256 | "exec": "npx npm-check-updates@16 --upgrade --target=latest --peer --dep=dev,peer,prod,optional --filter=cdklabs-projen-project-types,projen" 257 | }, 258 | { 259 | "exec": "yarn install --check-files" 260 | }, 261 | { 262 | "exec": "yarn upgrade cdklabs-projen-project-types projen" 263 | }, 264 | { 265 | "exec": "npx projen" 266 | }, 267 | { 268 | "spawn": "post-upgrade" 269 | } 270 | ] 271 | }, 272 | "upgrade-dev-deps": { 273 | "name": "upgrade-dev-deps", 274 | "description": "upgrade dev dependencies", 275 | "env": { 276 | "CI": "0" 277 | }, 278 | "steps": [ 279 | { 280 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --dep=dev --filter=@types/fs-extra,@types/jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-prettier,jest,prettier,ts-jest,ts-node,typescript" 281 | }, 282 | { 283 | "exec": "yarn install --check-files" 284 | }, 285 | { 286 | "exec": "yarn upgrade @types/fs-extra @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser commit-and-tag-version constructs eslint-config-prettier eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-prettier eslint jest jest-junit prettier ts-jest ts-node typescript" 287 | }, 288 | { 289 | "exec": "npx projen" 290 | }, 291 | { 292 | "spawn": "post-upgrade" 293 | } 294 | ] 295 | }, 296 | "watch": { 297 | "name": "watch", 298 | "description": "Watch & compile in the background", 299 | "steps": [ 300 | { 301 | "exec": "tsc --build -w" 302 | } 303 | ] 304 | } 305 | }, 306 | "env": { 307 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 308 | }, 309 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 310 | } 311 | -------------------------------------------------------------------------------- /.projenrc.ts: -------------------------------------------------------------------------------- 1 | import { CdklabsTypeScriptProject } from "cdklabs-projen-project-types"; 2 | 3 | const project = new CdklabsTypeScriptProject({ 4 | name: "cdk-app-cli", 5 | projenrcTs: true, 6 | private: false, 7 | enablePRAutoMerge: true, 8 | description: "The operator CLI for CDK applications.", 9 | repository: "https://github.com/cdklabs/cdk-app-cli", 10 | authorName: "Amazon Web Services", 11 | authorUrl: "https://aws.amazon.com", 12 | authorOrganization: true, 13 | defaultReleaseBranch: "main", 14 | 15 | bin: { 16 | "cdk-app": "bin/cdk-app", 17 | }, 18 | deps: [ 19 | "yargs", 20 | "fs-extra", 21 | "chalk@^4", 22 | "open", 23 | "yaml", 24 | "cdklabs-projen-project-types", 25 | ], 26 | devDeps: [ 27 | "@types/fs-extra", 28 | "ts-node@^10.9.1", 29 | "cdklabs-projen-project-types", 30 | ], 31 | prettier: true, 32 | minNodeVersion: "20.0.0", 33 | workflowNodeVersion: "20.x", 34 | releaseToNpm: true, 35 | }); 36 | 37 | project.synth(); 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cdk-app 2 | 3 | The operator CLI for CDK apps. Experimental. 4 | 5 | `cdk-app` lets you associate commands with CDK constructs so that you can quickly invoke functions, redrive queues, visit resources in the AWS console, and more by just referencing the construct name. 6 | 7 | ### Examples: 8 | 9 | * `cdk-app MyLambda tail-logs` - stream logs in real time from the Lambda's log group 10 | * `cdk-app TransactionTable visit-console` - open the AWS console page for your table 11 | * `cdk-app OrderQueue redrive-queue` - retry messages that failed to get processed 12 | 13 | ## 🚀 Getting Started 14 | 15 | Create a [CDK app](https://www.npmjs.com/package/aws-cdk) with a Lambda 16 | function, and deploy it to your AWS account. 17 | 18 | ```ts 19 | const fn = new lambda.Function(this, 'MyLambda', { 20 | code: lambda.Code.fromInline("exports.handler = async (event) => { console.log('event: ', event) };"), 21 | handler: 'index.handler', 22 | runtime: lambda.Runtime.NODEJS_14_X, 23 | }); 24 | ``` 25 | 26 | Next, `npm install -g cdk-app-cli` and view the available commands: 27 | 28 | ``` 29 | $ cdk-app MyLambda 30 | Commands: 31 | describe 32 | invoke 33 | visit-console 34 | audit-console 35 | ``` 36 | 37 | Run a command to instantly access the construct in the AWS console: 38 | 39 | ``` 40 | $ cdk-app MyLambda visit-console 41 | ``` 42 | 43 | Or run a command to get information in your command line: 44 | 45 | ``` 46 | $ cdk-app MyLambda describe 47 | > aws lambda get-function --function-name DemoAppStack-MyLambdaCCE802FB-lELPCJlktCim 48 | { 49 | "Configuration": { 50 | "FunctionName": "DemoAppStack-MyLambdaCCE802FB-lELPCJlktCim", 51 | ... 52 | ``` 53 | 54 | Any extra arguments you pass will get automatically passed through to the 55 | underlying command. 56 | 57 | You can add your own commands by defining a JSON or YAML file in your CDK app 58 | named `construct-commmands`. For example, here we define some commands that use 59 | the [awslogs](https://github.com/jorgebastida/awslogs) CLI: 60 | 61 | ```yaml 62 | # construct-commands.yml 63 | aws-cdk-lib.aws_lambda.Function: 64 | logs: 65 | exec: awslogs get /aws/lambda/${PHYSICAL_RESOURCE_ID} --start='5m ago' 66 | tail-logs: 67 | exec: awslogs get /aws/lambda/${PHYSICAL_RESOURCE_ID} ALL --watch 68 | ``` 69 | 70 | Now these commands will be available to use: 71 | 72 | ``` 73 | $ cdk-app MyLambda 74 | Commands: 75 | describe 76 | invoke 77 | visit-console 78 | audit-console 79 | logs 80 | tail-logs 81 | ``` 82 | 83 | Try using `cdk-app` in scripts as well! 84 | 85 | ```bash 86 | #!/usr/bin/env bash 87 | 88 | declare -a arr=("MyLambda" "MyTable" "MyQueue") 89 | 90 | for construct in "${arr[@]}"; do 91 | cdk-app "$construct" visit-console 92 | done 93 | ``` 94 | 95 | ## 📖 Documentation 96 | 97 | ### Command line options 98 | 99 | ``` 100 | cdk-app [construct] [subcommand] 101 | 102 | Run an operator command on your CDK app's constructs. 103 | 104 | Positionals: 105 | construct [string] [required] 106 | subcommand [string] 107 | 108 | Options: 109 | --version Show version number [boolean] 110 | --help Show help [boolean] 111 | 112 | Extra configuration options available via environment variables: 113 | 114 | - CDK_APP_DIR - Path to cdk.out. 115 | - AWS_REGION - AWS region to run commands in. 116 | ``` 117 | 118 | ### construct-commands.json 119 | 120 | #### Command types 121 | 122 | | Type | Description | 123 | | ----------- | ----------- | 124 | | open | Specify a URL or file to open. | 125 | | exec | Specify a command to run in your shell. | 126 | 127 | #### Command substitutions 128 | 129 | | Syntax | Description | 130 | | ----------- | ----------- | 131 | | `${AWS_REGION}` | The AWS_REGION of the current shell environment (e.g. `us-east-1`). | 132 | | `${PHYSICAL_RESOURCE_ID}` | The physical resource ID of construct's default resource. A construct named "Default" or "Resource" will automatically be assumed to be the default. | 133 | | `${URL_ENCODED_PHYSICAL_RESOURCE_ID}` | The same as `${PHYSICAL_RESOURCE_ID}`, except the value is URL encoded. | 134 | 135 | ## Security 136 | 137 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 138 | 139 | ## License 140 | 141 | This project is licensed under the Apache-2.0 License. 142 | -------------------------------------------------------------------------------- /bin/cdk-app: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../lib/cli.js'); 3 | -------------------------------------------------------------------------------- /construct-commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "aws-cdk-lib.aws_s3.Bucket": { 3 | "ls": { "exec": "aws s3 ls ${PHYSICAL_RESOURCE_ID}" }, 4 | "copy": { 5 | "exec": "aws s3api copy-object --bucket ${PHYSICAL_RESOURCE_ID}" 6 | }, 7 | "get": { 8 | "exec": "aws s3api get-object --bucket ${PHYSICAL_RESOURCE_ID}" 9 | }, 10 | "head": { 11 | "exec": "aws s3api head-object --bucket ${PHYSICAL_RESOURCE_ID}" 12 | }, 13 | "visit-console": { 14 | "open": "https://s3.console.aws.amazon.com/s3/buckets/${URL_ENCODED_PHYSICAL_RESOURCE_ID}?region=${AWS_REGION}&tab=objects" 15 | }, 16 | "audit": { 17 | "exec": "aws cloudtrail lookup-events --lookup-attributes AttributeKey=ResourceName,AttributeValue=${PHYSICAL_RESOURCE_ID}" 18 | }, 19 | "audit-console": { 20 | "open": "https://console.aws.amazon.com/cloudtrail/home?region=${AWS_REGION}#/events?ResourceName=${PHYSICAL_RESOURCE_ID}" 21 | } 22 | }, 23 | "aws-cdk-lib.aws_lambda.Function": { 24 | "describe": { 25 | "exec": "aws lambda get-function --function-name ${PHYSICAL_RESOURCE_ID}" 26 | }, 27 | "invoke": { 28 | "exec": "aws lambda invoke --function-name ${PHYSICAL_RESOURCE_ID} /dev/stdout" 29 | }, 30 | "visit-console": { 31 | "open": "https://console.aws.amazon.com/lambda/home?region=${AWS_REGION}#/functions/${URL_ENCODED_PHYSICAL_RESOURCE_ID}?tab=code" 32 | }, 33 | "audit": { 34 | "exec": "aws cloudtrail lookup-events --lookup-attributes AttributeKey=ResourceName,AttributeValue=${PHYSICAL_RESOURCE_ID}" 35 | }, 36 | "audit-console": { 37 | "open": "https://console.aws.amazon.com/cloudtrail/home?region=${AWS_REGION}#/events?ResourceName=${PHYSICAL_RESOURCE_ID}" 38 | } 39 | }, 40 | "aws-cdk-lib.aws_sqs.Queue": { 41 | "visit-console": { 42 | "open": "https://console.aws.amazon.com/sqs/v2/home?region=${AWS_REGION}#/queues/${URL_ENCODED_PHYSICAL_RESOURCE_ID}" 43 | }, 44 | "audit": { 45 | "exec": "aws cloudtrail lookup-events --lookup-attributes AttributeKey=ResourceName,AttributeValue=${PHYSICAL_RESOURCE_ID}" 46 | }, 47 | "audit-console": { 48 | "open": "https://console.aws.amazon.com/cloudtrail/home?region=${AWS_REGION}#/events?ResourceName=${PHYSICAL_RESOURCE_ID}" 49 | } 50 | }, 51 | "aws-cdk-lib.aws_sns.Topic": { 52 | "visit-console": { 53 | "open": "https://console.aws.amazon.com/sns/v3/home?region=${AWS_REGION}#/topic/${URL_ENCODED_PHYSICAL_RESOURCE_ID}" 54 | }, 55 | "audit": { 56 | "exec": "aws cloudtrail lookup-events --lookup-attributes AttributeKey=ResourceName,AttributeValue=${PHYSICAL_RESOURCE_ID}" 57 | }, 58 | "audit-console": { 59 | "open": "https://console.aws.amazon.com/cloudtrail/home?region=${AWS_REGION}#/events?ResourceName=${PHYSICAL_RESOURCE_ID}" 60 | } 61 | }, 62 | "aws-cdk-lib.aws_dynamodb.Table": { 63 | "describe": { 64 | "exec": "aws dynamodb describe-table --table-name ${PHYSICAL_RESOURCE_ID}" 65 | }, 66 | "scan": { 67 | "exec": "aws dynamodb scan --table-name ${PHYSICAL_RESOURCE_ID}" 68 | }, 69 | "visit-console": { 70 | "open": "https://console.aws.amazon.com/dynamodbv2/home?region=${AWS_REGION}#table?name=${URL_ENCODED_PHYSICAL_RESOURCE_ID}&tab=overview" 71 | }, 72 | "audit": { 73 | "exec": "aws cloudtrail lookup-events --lookup-attributes AttributeKey=ResourceName,AttributeValue=${PHYSICAL_RESOURCE_ID}" 74 | }, 75 | "audit-console": { 76 | "open": "https://console.aws.amazon.com/cloudtrail/home?region=${AWS_REGION}#/events?ResourceName=${PHYSICAL_RESOURCE_ID}" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-app-cli", 3 | "description": "The operator CLI for CDK applications.", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/cdklabs/cdk-app-cli" 7 | }, 8 | "bin": { 9 | "cdk-app": "bin/cdk-app" 10 | }, 11 | "scripts": { 12 | "build": "npx projen build", 13 | "bump": "npx projen bump", 14 | "clobber": "npx projen clobber", 15 | "compile": "npx projen compile", 16 | "default": "npx projen default", 17 | "eject": "npx projen eject", 18 | "eslint": "npx projen eslint", 19 | "package": "npx projen package", 20 | "post-compile": "npx projen post-compile", 21 | "post-upgrade": "npx projen post-upgrade", 22 | "pre-compile": "npx projen pre-compile", 23 | "release": "npx projen release", 24 | "test": "npx projen test", 25 | "test:watch": "npx projen test:watch", 26 | "unbump": "npx projen unbump", 27 | "upgrade": "npx projen upgrade", 28 | "upgrade-cdklabs-projen-project-types": "npx projen upgrade-cdklabs-projen-project-types", 29 | "upgrade-dev-deps": "npx projen upgrade-dev-deps", 30 | "watch": "npx projen watch", 31 | "projen": "npx projen" 32 | }, 33 | "author": { 34 | "name": "Amazon Web Services", 35 | "email": "aws-cdk-dev@amazon.com", 36 | "url": "https://aws.amazon.com", 37 | "organization": true 38 | }, 39 | "devDependencies": { 40 | "@types/fs-extra": "^9.0.13", 41 | "@types/jest": "^27.5.2", 42 | "@types/node": "^20", 43 | "@typescript-eslint/eslint-plugin": "^7", 44 | "@typescript-eslint/parser": "^7", 45 | "cdklabs-projen-project-types": "^0.1.202", 46 | "commit-and-tag-version": "^12", 47 | "constructs": "^10.0.0", 48 | "eslint": "^8", 49 | "eslint-config-prettier": "^8.10.0", 50 | "eslint-import-resolver-typescript": "^2.7.1", 51 | "eslint-plugin-import": "^2.31.0", 52 | "eslint-plugin-prettier": "^4.2.1", 53 | "jest": "^27.5.1", 54 | "jest-junit": "^15", 55 | "prettier": "^2.8.8", 56 | "projen": "^0.87.4", 57 | "ts-jest": "^27.1.5", 58 | "ts-node": "^10.9.2", 59 | "typescript": "^4.9.5" 60 | }, 61 | "dependencies": { 62 | "cdklabs-projen-project-types": "^0.1.204", 63 | "chalk": "^4", 64 | "fs-extra": "^10.1.0", 65 | "open": "^8.4.2", 66 | "yaml": "^2.8.0", 67 | "yargs": "^17.7.2" 68 | }, 69 | "engines": { 70 | "node": ">= 20.0.0" 71 | }, 72 | "main": "lib/index.js", 73 | "license": "Apache-2.0", 74 | "publishConfig": { 75 | "access": "public" 76 | }, 77 | "version": "0.0.0", 78 | "jest": { 79 | "coverageProvider": "v8", 80 | "testMatch": [ 81 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 82 | "/@(src|test)/**/__tests__/**/*.ts?(x)", 83 | "/@(projenrc)/**/*(*.)@(spec|test).ts?(x)", 84 | "/@(projenrc)/**/__tests__/**/*.ts?(x)" 85 | ], 86 | "clearMocks": true, 87 | "collectCoverage": true, 88 | "coverageReporters": [ 89 | "json", 90 | "lcov", 91 | "clover", 92 | "cobertura", 93 | "text" 94 | ], 95 | "coverageDirectory": "coverage", 96 | "coveragePathIgnorePatterns": [ 97 | "/node_modules/" 98 | ], 99 | "testPathIgnorePatterns": [ 100 | "/node_modules/" 101 | ], 102 | "watchPathIgnorePatterns": [ 103 | "/node_modules/" 104 | ], 105 | "reporters": [ 106 | "default", 107 | [ 108 | "jest-junit", 109 | { 110 | "outputDirectory": "test-reports" 111 | } 112 | ] 113 | ], 114 | "preset": "ts-jest", 115 | "globals": { 116 | "ts-jest": { 117 | "tsconfig": "tsconfig.dev.json" 118 | } 119 | } 120 | }, 121 | "types": "lib/index.d.ts", 122 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 123 | } 124 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import * as cp from "child_process"; 2 | import * as path from "path"; 3 | import chalk from "chalk"; 4 | import * as fs from "fs-extra"; 5 | import open from "open"; 6 | import { CommandStore } from "./command-store"; 7 | import { 8 | createCfnTemplateResourceNameMatcher, 9 | createTreeJsonResourceNameMatcher, 10 | createTreeJsonResourceTypeMatcher, 11 | } from "./matchers"; 12 | import { CloudAssembly, DescribeStackResourcesOutput, Resource } from "./model"; 13 | import { fail, execCapture, tryFindByPredicate, log } from "./util"; 14 | 15 | const commandStore = new CommandStore(); 16 | 17 | export interface CdkAppOptions { 18 | // Name of the construct as specified by the user in the CDK App 19 | readonly constructName: string; 20 | // Subcommand specified for the construct 21 | readonly subcommand?: string; 22 | // Path to the CDK app's Cloud Assembly 23 | readonly appDir: string; 24 | // Any arguments passed after "cdk-app " 25 | readonly restArgs: string[]; 26 | // AWS region to use 27 | readonly region: string; 28 | } 29 | 30 | export async function cdkApp(options: CdkAppOptions): Promise { 31 | log(options); 32 | 33 | const { 34 | constructName: nameOrPath, 35 | subcommand, 36 | appDir, 37 | region, 38 | restArgs, 39 | } = options; 40 | 41 | const { treeJson, cfnTemplate: templateJson } = getCloudAssembly(appDir); 42 | 43 | const stackName = getStackName(treeJson); 44 | 45 | // TODO: cache this for up to a day 46 | const stackResourceMetadata = getStackResourceMetadata(stackName, region); 47 | 48 | log(stackResourceMetadata); 49 | 50 | const resource = findResource( 51 | nameOrPath, 52 | { treeJson, cfnTemplate: templateJson }, 53 | stackResourceMetadata 54 | ); 55 | 56 | if (!resource) { 57 | throw new Error( 58 | `Could not find a resource named "${nameOrPath}" in the CDK app.` 59 | ); 60 | } 61 | 62 | if (!subcommand || subcommand === "help") { 63 | displayAvailableCommands(resource); 64 | return; 65 | } 66 | 67 | log(resource); 68 | 69 | await runCommand(resource, subcommand, region, restArgs); 70 | 71 | return; 72 | } 73 | 74 | function getCloudAssembly(appDir: string): CloudAssembly { 75 | if (!fs.existsSync(appDir)) { 76 | fail( 77 | new Error(`No cloud assembly found (expected to find it in "${appDir}").`) 78 | ); 79 | } 80 | const treeJson: any = fs.readJsonSync(path.join(appDir, "tree.json")); 81 | 82 | // TODO: improve logic for figuring out the correct template by checking 83 | // the manifest.json file 84 | const files: string[] = fs 85 | .readdirSync(appDir) 86 | .filter((f) => f.endsWith(".template.json")); 87 | if (files.length === 0) { 88 | fail( 89 | new Error( 90 | `Could not find any template.json files in the cloud assembly directory ("${appDir}").` 91 | ) 92 | ); 93 | } 94 | const templateFileName = files[0]; 95 | const cfnTemplate: any = fs.readJsonSync(path.join(appDir, templateFileName)); 96 | 97 | return { treeJson, cfnTemplate }; 98 | } 99 | 100 | function getStackResourceMetadata( 101 | stackName: string, 102 | region: string 103 | ): DescribeStackResourcesOutput { 104 | console.error("Refreshing stack metadata..."); 105 | let output = execCapture( 106 | `AWS_REGION=${region} aws cloudformation describe-stack-resources --stack-name ${stackName}`, 107 | { 108 | cwd: process.cwd(), 109 | } 110 | ); 111 | return JSON.parse(output.toString()); 112 | } 113 | 114 | function findResource( 115 | nameOrPath: string, 116 | cloudAssembly: CloudAssembly, 117 | stackResourceMetadata: DescribeStackResourcesOutput 118 | ): Resource | undefined { 119 | const { treeJson, cfnTemplate } = cloudAssembly; 120 | 121 | // TODO: throw error if multiple resources matching the nameOrPath are found 122 | const treeJsonMatcher = createTreeJsonResourceNameMatcher(nameOrPath); 123 | const treeData = tryFindByPredicate(treeJson, treeJsonMatcher); 124 | if (!treeData) { 125 | fail( 126 | new Error( 127 | `Could not find resource with name "${nameOrPath}" in tree.json.` 128 | ) 129 | ); 130 | } 131 | 132 | // TODO: handle case where resource ends with "/Default"? 133 | const cfnTemplateMatcher = createCfnTemplateResourceNameMatcher( 134 | treeData.value.path + "/Resource" 135 | ); 136 | const cfnTemplateData = tryFindByPredicate(cfnTemplate, cfnTemplateMatcher); 137 | if (!cfnTemplateData) { 138 | fail( 139 | new Error( 140 | `Could not find resource with aws:cdk:path metadata "${treeData.value.path}/Resource" in the CloudFormation template.` 141 | ) 142 | ); 143 | } 144 | 145 | const runtimeData = stackResourceMetadata.StackResources.find( 146 | (stackResource) => stackResource.LogicalResourceId === cfnTemplateData.key 147 | ); 148 | if (!runtimeData) { 149 | fail( 150 | new Error( 151 | `Could not find logical id "${cfnTemplateData.key}" in describe-stack-resources metadata.` 152 | ) 153 | ); 154 | } 155 | 156 | return { treeData: treeData.value, cfnTemplateData, runtimeData }; 157 | } 158 | 159 | export function displayAvailableCommands(resource: Resource) { 160 | const resourceType = resource.treeData.constructInfo?.fqn!; 161 | const commands = commandStore.commandsForResourceType(resourceType); 162 | 163 | if (!commands || Object.keys(commands).length === 0) { 164 | fail(new Error("No commands were found for this resource type.")); 165 | } 166 | 167 | console.log("Commands:"); 168 | for (const command of Object.keys(commands)) { 169 | console.log(` ${command}`); 170 | } 171 | } 172 | 173 | export async function runCommand( 174 | resource: Resource, 175 | subcommand: string, 176 | region: string, 177 | restArgs: string[] 178 | ) { 179 | const resourceType = resource.treeData.constructInfo?.fqn!; 180 | 181 | const command = commandStore.findCommand(resourceType, subcommand); 182 | 183 | if (command?.open) { 184 | const url = processSubstitutions(command.open, resource, region); 185 | console.error(chalk.gray(`> Opening "${url}"...`)); 186 | await open(url); 187 | } else if (command?.exec) { 188 | const baseCommand = processSubstitutions(command?.exec, resource, region); 189 | 190 | const args = restArgs.length > 0 ? " " + restArgs.join(" ") : ""; 191 | 192 | const fullCommand = `${baseCommand}${args}`; 193 | console.error(chalk.gray(`> ${fullCommand}`)); 194 | 195 | try { 196 | // pipe stdout and stderr in case of long running commands, like streaming logs 197 | const proc = cp.exec(fullCommand, { 198 | cwd: process.cwd(), 199 | env: { 200 | ...process.env, 201 | AWS_REGION: region, 202 | }, 203 | }); 204 | proc.stdout?.pipe(process.stdout); 205 | proc.stderr?.pipe(process.stderr); 206 | } catch (e: any) { 207 | fail(e as Error); 208 | } 209 | } 210 | } 211 | 212 | function getStackName(treeJson: any) { 213 | // TODO: what if there are multiple stacks? 214 | const stackMatcher = createTreeJsonResourceTypeMatcher("aws-cdk-lib.Stack"); 215 | const stackData = tryFindByPredicate(treeJson, stackMatcher); 216 | if (!stackData) { 217 | fail(new Error("Could not find a stack within in the CDK app.")); 218 | } 219 | return stackData.value.id; 220 | } 221 | 222 | function processSubstitutions( 223 | command: string, 224 | resource: Resource, 225 | region: string 226 | ) { 227 | command = command.replace( 228 | /\${URL_ENCODED_PHYSICAL_RESOURCE_ID}/g, 229 | encodeURIComponent(resource.runtimeData.PhysicalResourceId) 230 | ); 231 | command = command.replace( 232 | /\${PHYSICAL_RESOURCE_ID}/g, 233 | resource.runtimeData.PhysicalResourceId 234 | ); 235 | command = command.replace(/\${AWS_REGION}/g, region); 236 | return command; 237 | } 238 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | import yargs from "yargs"; 2 | import { cdkApp } from "./app"; 3 | 4 | export async function main() { 5 | const args: any = yargs 6 | .usage( 7 | "$0 [construct] [subcommand]", 8 | "Run an operator command on your CDK app's constructs.", 9 | (builder) => 10 | builder 11 | .positional("construct", { 12 | type: "string", 13 | description: 14 | "Name of a construct, or a full path to the construct.", 15 | }) 16 | .positional("subcommand", { 17 | type: "string", 18 | description: 19 | 'Operation to perform on the construct. Run "help" command for a list of available operations.', 20 | }) 21 | ) 22 | .demandOption(["construct"]) 23 | .epilogue( 24 | "Extra configuration options available via environment variables:\n\n- CDK_APP_DIR - Path to cdk.out.\n- AWS_REGION - AWS region to run commands in." 25 | ) 26 | .help().argv; 27 | 28 | // capture everything after `NODE cdk-app RESOURCE SUBCOMMAND` 29 | const restArgs = process.argv.slice(4); 30 | 31 | return cdkApp({ 32 | constructName: args.construct, 33 | subcommand: args.subcommand, 34 | appDir: process.env.CDK_APP_DIR ?? "cdk.out", 35 | region: 36 | process.env.AWS_REGION ?? 37 | process.env.CDK_DEFAULT_REGION ?? 38 | // TODO: infer region from cdk.out/manifest.json 39 | "us-east-1", 40 | restArgs, 41 | }); 42 | } 43 | 44 | main().catch((e) => { 45 | console.error(e); 46 | process.exit(1); 47 | }); 48 | -------------------------------------------------------------------------------- /src/command-store.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as fs from "fs-extra"; 3 | import * as yaml from "yaml"; 4 | import { deepMerge } from "./util"; 5 | 6 | export const CONSTRUCT_COMMANDS_FILES = [ 7 | "construct-commands.json", 8 | "construct-commands.yaml", 9 | "construct-commands.yml", 10 | ]; 11 | 12 | export class CommandStore { 13 | private readonly _commands: ConstructCommandsFile; 14 | constructor() { 15 | // load the default construct commands 16 | this._commands = fs.readJsonSync( 17 | path.join(__dirname, "..", "construct-commands.json") 18 | ); 19 | 20 | // check if user has defined custom commands 21 | const dir = process.cwd(); 22 | 23 | for (const filename of CONSTRUCT_COMMANDS_FILES) { 24 | const relative = path.join(dir, filename); 25 | if (fs.existsSync(relative)) { 26 | const extraCommands = yaml.parse( 27 | fs.readFileSync(relative).toString("utf-8") 28 | ); 29 | this._commands = deepMerge(this._commands, extraCommands); 30 | } 31 | } 32 | } 33 | 34 | public commandsForResourceType(fqn: string): ConstructCommands | undefined { 35 | return this._commands[fqn]; 36 | } 37 | 38 | public findCommand(fqn: string, subcommand: string): Command | undefined { 39 | const commands = this.commandsForResourceType(fqn); 40 | if (!commands) return undefined; 41 | 42 | const command = commands[subcommand]; 43 | return command; 44 | } 45 | } 46 | 47 | // Map from resource types to command list 48 | interface ConstructCommandsFile { 49 | [fqn: string]: ConstructCommands; 50 | } 51 | 52 | // Map from command names to command definitions 53 | interface ConstructCommands { 54 | [key: string]: Command; 55 | } 56 | 57 | interface Command { 58 | // run a process in the shell 59 | readonly exec?: string; 60 | // open a file or url 61 | readonly open?: string; 62 | } 63 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdklabs/cdk-app-cli/601ef587d03effbe6443386be02252745ea3cb52/src/index.ts -------------------------------------------------------------------------------- /src/matchers.ts: -------------------------------------------------------------------------------- 1 | import { TreeJsonResource } from "./model"; 2 | import { Condition } from "./util"; 3 | 4 | export function matchesPath(nameOrPath: string, resourcePath: string): boolean { 5 | return resourcePath.includes(nameOrPath); 6 | } 7 | 8 | export function createCfnTemplateResourceNameMatcher( 9 | resourcePath: string 10 | ): Condition { 11 | return (_key: string, value: any): value is any => 12 | value.hasOwnProperty("Metadata") && 13 | value.Metadata.hasOwnProperty("aws:cdk:path") && 14 | value.Metadata["aws:cdk:path"] === resourcePath; 15 | } 16 | 17 | export function createTreeJsonResourceNameMatcher( 18 | resourcePath: string 19 | ): Condition { 20 | return (_key: string, value: any): value is TreeJsonResource => 21 | value.hasOwnProperty("path") && value.path.includes(resourcePath); 22 | } 23 | 24 | export function createTreeJsonResourceTypeMatcher( 25 | resourceType: string 26 | ): Condition { 27 | return (_key: string, value: any): value is TreeJsonResource => 28 | value.hasOwnProperty("constructInfo") && 29 | value.constructInfo.hasOwnProperty("fqn") && 30 | value.constructInfo.fqn === resourceType; 31 | } 32 | -------------------------------------------------------------------------------- /src/model.ts: -------------------------------------------------------------------------------- 1 | export interface CloudAssembly { 2 | treeJson: TreeJson; 3 | cfnTemplate: any; 4 | } 5 | 6 | export interface Resource { 7 | treeData: TreeJsonResource; 8 | cfnTemplateData: any; // TODO: make more specific 9 | runtimeData: DescribeStackResourcesItem; 10 | } 11 | 12 | export interface TreeJson { 13 | readonly version: string; 14 | readonly tree: TreeJsonResource; 15 | } 16 | 17 | // TODO; make more specific / improve 18 | export interface TreeJsonResource { 19 | readonly id: string; 20 | readonly path: string; 21 | readonly children?: TreeJsonResource[]; 22 | readonly attributes?: any; 23 | readonly constructInfo?: TreeJsonConstructInfo; 24 | } 25 | 26 | export interface TreeJsonConstructInfo { 27 | readonly fqn: string; 28 | readonly version: string; 29 | } 30 | 31 | export interface DescribeStackResourcesOutput { 32 | readonly StackResources: DescribeStackResourcesItem[]; 33 | } 34 | 35 | export interface DescribeStackResourcesItem { 36 | readonly StackName: string; 37 | readonly StackId: string; 38 | readonly LogicalResourceId: string; 39 | readonly PhysicalResourceId: string; 40 | readonly ResourceType: string; 41 | readonly Timestamp: string; 42 | readonly ResourceStatus: string; // can make more specific 43 | readonly DriftInformation: any; // can make more specific 44 | } 45 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | import * as cp from "child_process"; 2 | import * as util from "util"; 3 | import chalk from "chalk"; 4 | 5 | const MAX_BUFFER = 10 * 1024 * 1024; 6 | 7 | const DEBUG = process.env.DEBUG; 8 | 9 | export function fail(e: Error): never { 10 | error(e.message); 11 | log(e.stack ?? "No stack trace."); 12 | process.exit(1); 13 | } 14 | 15 | export function error(message: any) { 16 | console.error(chalk.red(`[cdk-app] Error: ${message}`)); 17 | } 18 | 19 | export function log(message: any) { 20 | if (DEBUG) { 21 | if (typeof message === "string") { 22 | console.error(chalk.white(`[cdk-app] ${message}`)); 23 | } else { 24 | console.error(chalk.white(`[cdk-app] ${util.inspect(message)}`)); 25 | } 26 | } 27 | } 28 | 29 | /** 30 | * Executes a command with STDOUT > STDERR. 31 | */ 32 | export function exec(command: string, options: { cwd: string }): void { 33 | try { 34 | cp.execSync(command, { 35 | stdio: ["inherit", process.stderr, "pipe"], // "pipe" for STDERR means it appears in exceptions 36 | maxBuffer: MAX_BUFFER, 37 | cwd: options.cwd, 38 | }); 39 | } catch (e) { 40 | fail(e as Error); 41 | } 42 | } 43 | 44 | /** 45 | * Executes command and returns STDOUT. If the command fails (non-zero), throws an error. 46 | */ 47 | export function execCapture(command: string, options: { cwd: string }) { 48 | try { 49 | return cp.execSync(command, { 50 | stdio: ["inherit", "pipe", "pipe"], // "pipe" for STDERR means it appears in exceptions 51 | maxBuffer: MAX_BUFFER, 52 | cwd: options.cwd, 53 | }); 54 | } catch (e) { 55 | fail(e as Error); 56 | } 57 | } 58 | 59 | type Obj = { [key: string]: any }; 60 | 61 | function isObj(value: any): value is Obj { 62 | return typeof value === "object" && value != null && !Array.isArray(value); 63 | } 64 | 65 | export function deepMerge(target: any, source: any) { 66 | let output = Object.assign({}, target); 67 | if (isObj(target) && isObj(source)) { 68 | Object.keys(source).forEach((key) => { 69 | if (isObj(source[key])) { 70 | if (!(key in target)) Object.assign(output, { [key]: source[key] }); 71 | else output[key] = deepMerge(target[key], source[key]); 72 | } else { 73 | Object.assign(output, { [key]: source[key] }); 74 | } 75 | }); 76 | } 77 | return output; 78 | } 79 | 80 | export type Condition = (key: string, value: any) => value is T; 81 | 82 | /** 83 | * Given a JSON object and a matching function, find the section of 84 | * JSON satisfying the condition if there is one. 85 | */ 86 | export function tryFindByPredicate( 87 | json: any, 88 | condition: Condition 89 | ): { key: string; value: T } | undefined { 90 | if (!isObj(json)) { 91 | return undefined; 92 | } 93 | 94 | for (const [key, value] of Object.entries(json)) { 95 | if (isObj(value) && condition(key, value)) { 96 | return { key, value }; 97 | } 98 | 99 | const resource = tryFindByPredicate(value, condition); 100 | if (resource) { 101 | return resource; 102 | } 103 | } 104 | 105 | return undefined; 106 | } 107 | -------------------------------------------------------------------------------- /test/fixtures/DemoAppStack.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Resources": { 3 | "MyQueueE6CA6235": { 4 | "Type": "AWS::SQS::Queue", 5 | "Properties": { 6 | "VisibilityTimeout": 300 7 | }, 8 | "UpdateReplacePolicy": "Delete", 9 | "DeletionPolicy": "Delete", 10 | "Metadata": { 11 | "aws:cdk:path": "DemoAppStack/MyQueue/Resource" 12 | } 13 | }, 14 | "MyQueuePolicy6BBEDDAC": { 15 | "Type": "AWS::SQS::QueuePolicy", 16 | "Properties": { 17 | "PolicyDocument": { 18 | "Statement": [ 19 | { 20 | "Action": "sqs:SendMessage", 21 | "Condition": { 22 | "ArnEquals": { 23 | "aws:SourceArn": { 24 | "Ref": "MyTopic86869434" 25 | } 26 | } 27 | }, 28 | "Effect": "Allow", 29 | "Principal": { 30 | "Service": "sns.amazonaws.com" 31 | }, 32 | "Resource": { 33 | "Fn::GetAtt": ["MyQueueE6CA6235", "Arn"] 34 | } 35 | } 36 | ], 37 | "Version": "2012-10-17" 38 | }, 39 | "Queues": [ 40 | { 41 | "Ref": "MyQueueE6CA6235" 42 | } 43 | ] 44 | }, 45 | "Metadata": { 46 | "aws:cdk:path": "DemoAppStack/MyQueue/Policy/Resource" 47 | } 48 | }, 49 | "MyQueueDemoAppStackMyTopic242DB4D11760FF8B": { 50 | "Type": "AWS::SNS::Subscription", 51 | "Properties": { 52 | "Protocol": "sqs", 53 | "TopicArn": { 54 | "Ref": "MyTopic86869434" 55 | }, 56 | "Endpoint": { 57 | "Fn::GetAtt": ["MyQueueE6CA6235", "Arn"] 58 | } 59 | }, 60 | "Metadata": { 61 | "aws:cdk:path": "DemoAppStack/MyQueue/DemoAppStackMyTopic242DB4D1/Resource" 62 | } 63 | }, 64 | "MyTopic86869434": { 65 | "Type": "AWS::SNS::Topic", 66 | "Metadata": { 67 | "aws:cdk:path": "DemoAppStack/MyTopic/Resource" 68 | } 69 | }, 70 | "MyLambdaServiceRole4539ECB6": { 71 | "Type": "AWS::IAM::Role", 72 | "Properties": { 73 | "AssumeRolePolicyDocument": { 74 | "Statement": [ 75 | { 76 | "Action": "sts:AssumeRole", 77 | "Effect": "Allow", 78 | "Principal": { 79 | "Service": "lambda.amazonaws.com" 80 | } 81 | } 82 | ], 83 | "Version": "2012-10-17" 84 | }, 85 | "ManagedPolicyArns": [ 86 | { 87 | "Fn::Join": [ 88 | "", 89 | [ 90 | "arn:", 91 | { 92 | "Ref": "AWS::Partition" 93 | }, 94 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 95 | ] 96 | ] 97 | } 98 | ] 99 | }, 100 | "Metadata": { 101 | "aws:cdk:path": "DemoAppStack/MyLambda/ServiceRole/Resource" 102 | } 103 | }, 104 | "MyLambdaCCE802FB": { 105 | "Type": "AWS::Lambda::Function", 106 | "Properties": { 107 | "Code": { 108 | "S3Bucket": { 109 | "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" 110 | }, 111 | "S3Key": "30e57c6d236b8baf7f6d2b9178a5436cc8bb46b95538f407ab36ab2df819cdd3.zip" 112 | }, 113 | "Role": { 114 | "Fn::GetAtt": ["MyLambdaServiceRole4539ECB6", "Arn"] 115 | }, 116 | "Handler": "index.handler", 117 | "Runtime": "nodejs14.x" 118 | }, 119 | "DependsOn": ["MyLambdaServiceRole4539ECB6"], 120 | "Metadata": { 121 | "aws:cdk:path": "DemoAppStack/MyLambda/Resource", 122 | "aws:asset:path": "asset.30e57c6d236b8baf7f6d2b9178a5436cc8bb46b95538f407ab36ab2df819cdd3", 123 | "aws:asset:is-bundled": false, 124 | "aws:asset:property": "Code" 125 | } 126 | }, 127 | "MyBucketF68F3FF0": { 128 | "Type": "AWS::S3::Bucket", 129 | "Properties": { 130 | "BucketEncryption": { 131 | "ServerSideEncryptionConfiguration": [ 132 | { 133 | "ServerSideEncryptionByDefault": { 134 | "SSEAlgorithm": "AES256" 135 | } 136 | } 137 | ] 138 | } 139 | }, 140 | "UpdateReplacePolicy": "Retain", 141 | "DeletionPolicy": "Retain", 142 | "Metadata": { 143 | "aws:cdk:path": "DemoAppStack/MyBucket/Resource" 144 | } 145 | }, 146 | "CDKMetadata": { 147 | "Type": "AWS::CDK::Metadata", 148 | "Properties": { 149 | "Analytics": "v2:deflate64:H4sIAAAAAAAA/1VQyQ7CIBD9Fu90XKNnNfHsdm8oRTO2hdoBjSH8uyypiZd5S97MCyxgvoLZhL+pEHVTtFiBuxguGhas0tGTwJ2stJLtbyqTNI+6RfH5mVl6RirkL7YiMWBvUKuY+NNX3aOIbiKetbyrag7uYJUYF0buGfIO3Fm3qT5iqFiWnEgagm2EoMHtrGik2XGSLNMYz8x7lnLhUXdU93RHkraDSDf3WtWYu5SuJTxo+ppvYB3+5EGIxWCVwU7COeMXhjd7DC8BAAA=" 150 | }, 151 | "Metadata": { 152 | "aws:cdk:path": "DemoAppStack/CDKMetadata/Default" 153 | }, 154 | "Condition": "CDKMetadataAvailable" 155 | } 156 | }, 157 | "Conditions": { 158 | "CDKMetadataAvailable": { 159 | "Fn::Or": [ 160 | { 161 | "Fn::Or": [ 162 | { 163 | "Fn::Equals": [ 164 | { 165 | "Ref": "AWS::Region" 166 | }, 167 | "af-south-1" 168 | ] 169 | }, 170 | { 171 | "Fn::Equals": [ 172 | { 173 | "Ref": "AWS::Region" 174 | }, 175 | "ap-east-1" 176 | ] 177 | }, 178 | { 179 | "Fn::Equals": [ 180 | { 181 | "Ref": "AWS::Region" 182 | }, 183 | "ap-northeast-1" 184 | ] 185 | }, 186 | { 187 | "Fn::Equals": [ 188 | { 189 | "Ref": "AWS::Region" 190 | }, 191 | "ap-northeast-2" 192 | ] 193 | }, 194 | { 195 | "Fn::Equals": [ 196 | { 197 | "Ref": "AWS::Region" 198 | }, 199 | "ap-south-1" 200 | ] 201 | }, 202 | { 203 | "Fn::Equals": [ 204 | { 205 | "Ref": "AWS::Region" 206 | }, 207 | "ap-southeast-1" 208 | ] 209 | }, 210 | { 211 | "Fn::Equals": [ 212 | { 213 | "Ref": "AWS::Region" 214 | }, 215 | "ap-southeast-2" 216 | ] 217 | }, 218 | { 219 | "Fn::Equals": [ 220 | { 221 | "Ref": "AWS::Region" 222 | }, 223 | "ca-central-1" 224 | ] 225 | }, 226 | { 227 | "Fn::Equals": [ 228 | { 229 | "Ref": "AWS::Region" 230 | }, 231 | "cn-north-1" 232 | ] 233 | }, 234 | { 235 | "Fn::Equals": [ 236 | { 237 | "Ref": "AWS::Region" 238 | }, 239 | "cn-northwest-1" 240 | ] 241 | } 242 | ] 243 | }, 244 | { 245 | "Fn::Or": [ 246 | { 247 | "Fn::Equals": [ 248 | { 249 | "Ref": "AWS::Region" 250 | }, 251 | "eu-central-1" 252 | ] 253 | }, 254 | { 255 | "Fn::Equals": [ 256 | { 257 | "Ref": "AWS::Region" 258 | }, 259 | "eu-north-1" 260 | ] 261 | }, 262 | { 263 | "Fn::Equals": [ 264 | { 265 | "Ref": "AWS::Region" 266 | }, 267 | "eu-south-1" 268 | ] 269 | }, 270 | { 271 | "Fn::Equals": [ 272 | { 273 | "Ref": "AWS::Region" 274 | }, 275 | "eu-west-1" 276 | ] 277 | }, 278 | { 279 | "Fn::Equals": [ 280 | { 281 | "Ref": "AWS::Region" 282 | }, 283 | "eu-west-2" 284 | ] 285 | }, 286 | { 287 | "Fn::Equals": [ 288 | { 289 | "Ref": "AWS::Region" 290 | }, 291 | "eu-west-3" 292 | ] 293 | }, 294 | { 295 | "Fn::Equals": [ 296 | { 297 | "Ref": "AWS::Region" 298 | }, 299 | "me-south-1" 300 | ] 301 | }, 302 | { 303 | "Fn::Equals": [ 304 | { 305 | "Ref": "AWS::Region" 306 | }, 307 | "sa-east-1" 308 | ] 309 | }, 310 | { 311 | "Fn::Equals": [ 312 | { 313 | "Ref": "AWS::Region" 314 | }, 315 | "us-east-1" 316 | ] 317 | }, 318 | { 319 | "Fn::Equals": [ 320 | { 321 | "Ref": "AWS::Region" 322 | }, 323 | "us-east-2" 324 | ] 325 | } 326 | ] 327 | }, 328 | { 329 | "Fn::Or": [ 330 | { 331 | "Fn::Equals": [ 332 | { 333 | "Ref": "AWS::Region" 334 | }, 335 | "us-west-1" 336 | ] 337 | }, 338 | { 339 | "Fn::Equals": [ 340 | { 341 | "Ref": "AWS::Region" 342 | }, 343 | "us-west-2" 344 | ] 345 | } 346 | ] 347 | } 348 | ] 349 | } 350 | }, 351 | "Parameters": { 352 | "BootstrapVersion": { 353 | "Type": "AWS::SSM::Parameter::Value", 354 | "Default": "/cdk-bootstrap/hnb659fds/version", 355 | "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" 356 | } 357 | }, 358 | "Rules": { 359 | "CheckBootstrapVersion": { 360 | "Assertions": [ 361 | { 362 | "Assert": { 363 | "Fn::Not": [ 364 | { 365 | "Fn::Contains": [ 366 | ["1", "2", "3", "4", "5"], 367 | { 368 | "Ref": "BootstrapVersion" 369 | } 370 | ] 371 | } 372 | ] 373 | }, 374 | "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." 375 | } 376 | ] 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /test/fixtures/tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "tree-0.1", 3 | "tree": { 4 | "id": "App", 5 | "path": "", 6 | "children": { 7 | "Tree": { 8 | "id": "Tree", 9 | "path": "Tree", 10 | "constructInfo": { 11 | "fqn": "constructs.Construct", 12 | "version": "10.0.74" 13 | } 14 | }, 15 | "DemoAppStack": { 16 | "id": "DemoAppStack", 17 | "path": "DemoAppStack", 18 | "children": { 19 | "MyQueue": { 20 | "id": "MyQueue", 21 | "path": "DemoAppStack/MyQueue", 22 | "children": { 23 | "Resource": { 24 | "id": "Resource", 25 | "path": "DemoAppStack/MyQueue/Resource", 26 | "attributes": { 27 | "aws:cdk:cloudformation:type": "AWS::SQS::Queue", 28 | "aws:cdk:cloudformation:props": { 29 | "visibilityTimeout": 300 30 | } 31 | }, 32 | "constructInfo": { 33 | "fqn": "aws-cdk-lib.aws_sqs.CfnQueue", 34 | "version": "2.14.0" 35 | } 36 | }, 37 | "Policy": { 38 | "id": "Policy", 39 | "path": "DemoAppStack/MyQueue/Policy", 40 | "children": { 41 | "Resource": { 42 | "id": "Resource", 43 | "path": "DemoAppStack/MyQueue/Policy/Resource", 44 | "attributes": { 45 | "aws:cdk:cloudformation:type": "AWS::SQS::QueuePolicy", 46 | "aws:cdk:cloudformation:props": { 47 | "policyDocument": { 48 | "Statement": [ 49 | { 50 | "Action": "sqs:SendMessage", 51 | "Condition": { 52 | "ArnEquals": { 53 | "aws:SourceArn": { 54 | "Ref": "MyTopic86869434" 55 | } 56 | } 57 | }, 58 | "Effect": "Allow", 59 | "Principal": { 60 | "Service": "sns.amazonaws.com" 61 | }, 62 | "Resource": { 63 | "Fn::GetAtt": ["MyQueueE6CA6235", "Arn"] 64 | } 65 | } 66 | ], 67 | "Version": "2012-10-17" 68 | }, 69 | "queues": [ 70 | { 71 | "Ref": "MyQueueE6CA6235" 72 | } 73 | ] 74 | } 75 | }, 76 | "constructInfo": { 77 | "fqn": "aws-cdk-lib.aws_sqs.CfnQueuePolicy", 78 | "version": "2.14.0" 79 | } 80 | } 81 | }, 82 | "constructInfo": { 83 | "fqn": "aws-cdk-lib.aws_sqs.QueuePolicy", 84 | "version": "2.14.0" 85 | } 86 | }, 87 | "DemoAppStackMyTopic242DB4D1": { 88 | "id": "DemoAppStackMyTopic242DB4D1", 89 | "path": "DemoAppStack/MyQueue/DemoAppStackMyTopic242DB4D1", 90 | "children": { 91 | "Resource": { 92 | "id": "Resource", 93 | "path": "DemoAppStack/MyQueue/DemoAppStackMyTopic242DB4D1/Resource", 94 | "attributes": { 95 | "aws:cdk:cloudformation:type": "AWS::SNS::Subscription", 96 | "aws:cdk:cloudformation:props": { 97 | "protocol": "sqs", 98 | "topicArn": { 99 | "Ref": "MyTopic86869434" 100 | }, 101 | "endpoint": { 102 | "Fn::GetAtt": ["MyQueueE6CA6235", "Arn"] 103 | } 104 | } 105 | }, 106 | "constructInfo": { 107 | "fqn": "aws-cdk-lib.aws_sns.CfnSubscription", 108 | "version": "2.14.0" 109 | } 110 | } 111 | }, 112 | "constructInfo": { 113 | "fqn": "aws-cdk-lib.aws_sns.Subscription", 114 | "version": "2.14.0" 115 | } 116 | } 117 | }, 118 | "constructInfo": { 119 | "fqn": "aws-cdk-lib.aws_sqs.Queue", 120 | "version": "2.14.0" 121 | } 122 | }, 123 | "MyTopic": { 124 | "id": "MyTopic", 125 | "path": "DemoAppStack/MyTopic", 126 | "children": { 127 | "Resource": { 128 | "id": "Resource", 129 | "path": "DemoAppStack/MyTopic/Resource", 130 | "attributes": { 131 | "aws:cdk:cloudformation:type": "AWS::SNS::Topic", 132 | "aws:cdk:cloudformation:props": {} 133 | }, 134 | "constructInfo": { 135 | "fqn": "aws-cdk-lib.aws_sns.CfnTopic", 136 | "version": "2.14.0" 137 | } 138 | } 139 | }, 140 | "constructInfo": { 141 | "fqn": "aws-cdk-lib.aws_sns.Topic", 142 | "version": "2.14.0" 143 | } 144 | }, 145 | "MyLambda": { 146 | "id": "MyLambda", 147 | "path": "DemoAppStack/MyLambda", 148 | "children": { 149 | "ServiceRole": { 150 | "id": "ServiceRole", 151 | "path": "DemoAppStack/MyLambda/ServiceRole", 152 | "children": { 153 | "Resource": { 154 | "id": "Resource", 155 | "path": "DemoAppStack/MyLambda/ServiceRole/Resource", 156 | "attributes": { 157 | "aws:cdk:cloudformation:type": "AWS::IAM::Role", 158 | "aws:cdk:cloudformation:props": { 159 | "assumeRolePolicyDocument": { 160 | "Statement": [ 161 | { 162 | "Action": "sts:AssumeRole", 163 | "Effect": "Allow", 164 | "Principal": { 165 | "Service": "lambda.amazonaws.com" 166 | } 167 | } 168 | ], 169 | "Version": "2012-10-17" 170 | }, 171 | "managedPolicyArns": [ 172 | { 173 | "Fn::Join": [ 174 | "", 175 | [ 176 | "arn:", 177 | { 178 | "Ref": "AWS::Partition" 179 | }, 180 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 181 | ] 182 | ] 183 | } 184 | ] 185 | } 186 | }, 187 | "constructInfo": { 188 | "fqn": "aws-cdk-lib.aws_iam.CfnRole", 189 | "version": "2.14.0" 190 | } 191 | } 192 | }, 193 | "constructInfo": { 194 | "fqn": "aws-cdk-lib.aws_iam.Role", 195 | "version": "2.14.0" 196 | } 197 | }, 198 | "Code": { 199 | "id": "Code", 200 | "path": "DemoAppStack/MyLambda/Code", 201 | "children": { 202 | "Stage": { 203 | "id": "Stage", 204 | "path": "DemoAppStack/MyLambda/Code/Stage", 205 | "constructInfo": { 206 | "fqn": "aws-cdk-lib.AssetStaging", 207 | "version": "2.14.0" 208 | } 209 | }, 210 | "AssetBucket": { 211 | "id": "AssetBucket", 212 | "path": "DemoAppStack/MyLambda/Code/AssetBucket", 213 | "constructInfo": { 214 | "fqn": "aws-cdk-lib.aws_s3.BucketBase", 215 | "version": "2.14.0" 216 | } 217 | } 218 | }, 219 | "constructInfo": { 220 | "fqn": "aws-cdk-lib.aws_s3_assets.Asset", 221 | "version": "2.14.0" 222 | } 223 | }, 224 | "Resource": { 225 | "id": "Resource", 226 | "path": "DemoAppStack/MyLambda/Resource", 227 | "attributes": { 228 | "aws:cdk:cloudformation:type": "AWS::Lambda::Function", 229 | "aws:cdk:cloudformation:props": { 230 | "code": { 231 | "s3Bucket": { 232 | "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" 233 | }, 234 | "s3Key": "30e57c6d236b8baf7f6d2b9178a5436cc8bb46b95538f407ab36ab2df819cdd3.zip" 235 | }, 236 | "role": { 237 | "Fn::GetAtt": ["MyLambdaServiceRole4539ECB6", "Arn"] 238 | }, 239 | "handler": "index.handler", 240 | "runtime": "nodejs14.x" 241 | } 242 | }, 243 | "constructInfo": { 244 | "fqn": "aws-cdk-lib.aws_lambda.CfnFunction", 245 | "version": "2.14.0" 246 | } 247 | } 248 | }, 249 | "constructInfo": { 250 | "fqn": "aws-cdk-lib.aws_lambda.Function", 251 | "version": "2.14.0" 252 | } 253 | }, 254 | "MyBucket": { 255 | "id": "MyBucket", 256 | "path": "DemoAppStack/MyBucket", 257 | "children": { 258 | "Resource": { 259 | "id": "Resource", 260 | "path": "DemoAppStack/MyBucket/Resource", 261 | "attributes": { 262 | "aws:cdk:cloudformation:type": "AWS::S3::Bucket", 263 | "aws:cdk:cloudformation:props": { 264 | "bucketEncryption": { 265 | "serverSideEncryptionConfiguration": [ 266 | { 267 | "serverSideEncryptionByDefault": { 268 | "sseAlgorithm": "AES256" 269 | } 270 | } 271 | ] 272 | } 273 | } 274 | }, 275 | "constructInfo": { 276 | "fqn": "aws-cdk-lib.aws_s3.CfnBucket", 277 | "version": "2.14.0" 278 | } 279 | } 280 | }, 281 | "constructInfo": { 282 | "fqn": "aws-cdk-lib.aws_s3.Bucket", 283 | "version": "2.14.0" 284 | } 285 | }, 286 | "CDKMetadata": { 287 | "id": "CDKMetadata", 288 | "path": "DemoAppStack/CDKMetadata", 289 | "children": { 290 | "Default": { 291 | "id": "Default", 292 | "path": "DemoAppStack/CDKMetadata/Default", 293 | "constructInfo": { 294 | "fqn": "aws-cdk-lib.CfnResource", 295 | "version": "2.14.0" 296 | } 297 | }, 298 | "Condition": { 299 | "id": "Condition", 300 | "path": "DemoAppStack/CDKMetadata/Condition", 301 | "constructInfo": { 302 | "fqn": "aws-cdk-lib.CfnCondition", 303 | "version": "2.14.0" 304 | } 305 | } 306 | }, 307 | "constructInfo": { 308 | "fqn": "constructs.Construct", 309 | "version": "10.0.74" 310 | } 311 | } 312 | }, 313 | "constructInfo": { 314 | "fqn": "aws-cdk-lib.Stack", 315 | "version": "2.14.0" 316 | } 317 | } 318 | }, 319 | "constructInfo": { 320 | "fqn": "aws-cdk-lib.App", 321 | "version": "2.14.0" 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /test/matchers.test.ts: -------------------------------------------------------------------------------- 1 | import templateFixture from "./fixtures/DemoAppStack.template.json"; 2 | import treeFixture from "./fixtures/tree.json"; 3 | import { 4 | createCfnTemplateResourceNameMatcher, 5 | createTreeJsonResourceNameMatcher, 6 | createTreeJsonResourceTypeMatcher, 7 | matchesPath, 8 | } from "../src/matchers"; 9 | import { tryFindByPredicate } from "../src/util"; 10 | 11 | test("matchesPath", () => { 12 | expect(matchesPath("MyQueue", "DemoAppStack")).toEqual(false); 13 | expect(matchesPath("MyQueue", "DemoAppStack/MyQueue")).toEqual(true); 14 | expect(matchesPath("MyQueue", "MyQueue/Resource")).toEqual(true); 15 | expect(matchesPath("MyQueue", "DemoAppStack/MyQueue/Resource")).toEqual(true); 16 | }); 17 | 18 | test("createTreeJsonResourceNameMatcher", () => { 19 | const treeJsonMatcher = createTreeJsonResourceNameMatcher("MyQueue"); 20 | expect(tryFindByPredicate(treeFixture, treeJsonMatcher)).toEqual({ 21 | key: "MyQueue", 22 | value: expect.objectContaining({ 23 | id: "MyQueue", 24 | path: "DemoAppStack/MyQueue", 25 | }), 26 | }); 27 | }); 28 | 29 | test("createTreeJsonResourceTypeMatcher", () => { 30 | const treeJsonMatcher = 31 | createTreeJsonResourceTypeMatcher("aws-cdk-lib.Stack"); 32 | expect(tryFindByPredicate(treeFixture, treeJsonMatcher)).toEqual({ 33 | key: "DemoAppStack", 34 | value: expect.objectContaining({ 35 | id: "DemoAppStack", 36 | path: "DemoAppStack", 37 | }), 38 | }); 39 | }); 40 | 41 | test("createCfnTemplateResourceNameMatcher", () => { 42 | const cfnTemplateMatcher = createCfnTemplateResourceNameMatcher( 43 | "DemoAppStack/MyQueue/Resource" 44 | ); 45 | expect(tryFindByPredicate(templateFixture, cfnTemplateMatcher)).toEqual({ 46 | key: "MyQueueE6CA6235", 47 | value: expect.objectContaining({ 48 | Type: "AWS::SQS::Queue", 49 | Metadata: { 50 | "aws:cdk:path": "DemoAppStack/MyQueue/Resource", 51 | }, 52 | }), 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2019" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2019" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.ts", 32 | "projenrc/**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "lib", 6 | "alwaysStrict": true, 7 | "declaration": true, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "inlineSourceMap": true, 11 | "inlineSources": true, 12 | "lib": [ 13 | "es2019" 14 | ], 15 | "module": "CommonJS", 16 | "noEmitOnError": false, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitReturns": true, 20 | "noImplicitThis": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "resolveJsonModule": true, 24 | "strict": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "stripInternal": true, 28 | "target": "ES2019" 29 | }, 30 | "include": [ 31 | "src/**/*.ts" 32 | ], 33 | "exclude": [] 34 | } 35 | --------------------------------------------------------------------------------