├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── build-sample.yml │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release-v1-main.yml │ ├── release.yml │ ├── upgrade-main.yml │ └── upgrade-v1-main.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.js ├── API.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── package.json ├── sample-pipeline ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .npmignore ├── .projen │ ├── deps.json │ ├── files.json │ └── tasks.json ├── README.md ├── cdk.json ├── package.json ├── src │ ├── app.ts │ ├── app │ │ ├── Dockerfile │ │ └── index.html │ ├── lambda │ │ ├── index.ts │ │ └── msg.txt │ └── pipeline.ts ├── test │ ├── __snapshots__ │ │ └── pipeline.test.ts.snap │ └── pipeline.test.ts ├── tsconfig.dev.json ├── tsconfig.json └── yarn.lock ├── sample ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .npmignore ├── .projen │ ├── deps.json │ ├── files.json │ └── tasks.json ├── README.md ├── cdk.json ├── cdk.out │ ├── my-stack-dev.assets.json │ ├── my-stack-dev.template.json │ ├── my-stack-dev2.assets.json │ └── my-stack-dev2.template.json ├── docker │ └── Dockerfile ├── lambda │ ├── package-lock.json │ ├── package.json │ └── src │ │ └── index.js ├── package.json ├── src │ └── main.ts ├── test │ ├── __snapshots__ │ │ └── main.test.ts.snap │ └── main.test.ts ├── tsconfig.dev.json ├── tsconfig.json └── yarn.lock ├── scripts └── extractdoc.py ├── src ├── aspect.ts ├── index.ts └── template.yml ├── test ├── BootstraplessStackSynthesizer.test.ts └── utils.ts ├── tsconfig.dev.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js 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 | ], 21 | "settings": { 22 | "import/parsers": { 23 | "@typescript-eslint/parser": [ 24 | ".ts", 25 | ".tsx" 26 | ] 27 | }, 28 | "import/resolver": { 29 | "node": {}, 30 | "typescript": { 31 | "project": "./tsconfig.dev.json", 32 | "alwaysTryTypes": true 33 | } 34 | } 35 | }, 36 | "ignorePatterns": [ 37 | "*.js", 38 | "!.projenrc.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage" 43 | ], 44 | "rules": { 45 | "indent": [ 46 | "off" 47 | ], 48 | "@typescript-eslint/indent": [ 49 | "error", 50 | 2 51 | ], 52 | "quotes": [ 53 | "error", 54 | "single", 55 | { 56 | "avoidEscape": true 57 | } 58 | ], 59 | "comma-dangle": [ 60 | "error", 61 | "always-multiline" 62 | ], 63 | "comma-spacing": [ 64 | "error", 65 | { 66 | "before": false, 67 | "after": true 68 | } 69 | ], 70 | "no-multi-spaces": [ 71 | "error", 72 | { 73 | "ignoreEOLComments": false 74 | } 75 | ], 76 | "array-bracket-spacing": [ 77 | "error", 78 | "never" 79 | ], 80 | "array-bracket-newline": [ 81 | "error", 82 | "consistent" 83 | ], 84 | "object-curly-spacing": [ 85 | "error", 86 | "always" 87 | ], 88 | "object-curly-newline": [ 89 | "error", 90 | { 91 | "multiline": true, 92 | "consistent": true 93 | } 94 | ], 95 | "object-property-newline": [ 96 | "error", 97 | { 98 | "allowAllPropertiesOnSameLine": true 99 | } 100 | ], 101 | "keyword-spacing": [ 102 | "error" 103 | ], 104 | "brace-style": [ 105 | "error", 106 | "1tbs", 107 | { 108 | "allowSingleLine": true 109 | } 110 | ], 111 | "space-before-blocks": [ 112 | "error" 113 | ], 114 | "curly": [ 115 | "error", 116 | "multi-line", 117 | "consistent" 118 | ], 119 | "@typescript-eslint/member-delimiter-style": [ 120 | "error" 121 | ], 122 | "semi": [ 123 | "error", 124 | "always" 125 | ], 126 | "max-len": [ 127 | "error", 128 | { 129 | "code": 150, 130 | "ignoreUrls": true, 131 | "ignoreStrings": true, 132 | "ignoreTemplateLiterals": true, 133 | "ignoreComments": true, 134 | "ignoreRegExpLiterals": true 135 | } 136 | ], 137 | "quote-props": [ 138 | "error", 139 | "consistent-as-needed" 140 | ], 141 | "@typescript-eslint/no-require-imports": [ 142 | "error" 143 | ], 144 | "import/no-extraneous-dependencies": [ 145 | "error", 146 | { 147 | "devDependencies": [ 148 | "**/test/**", 149 | "**/build-tools/**" 150 | ], 151 | "optionalDependencies": false, 152 | "peerDependencies": true 153 | } 154 | ], 155 | "import/no-unresolved": [ 156 | "error" 157 | ], 158 | "import/order": [ 159 | "warn", 160 | { 161 | "groups": [ 162 | "builtin", 163 | "external" 164 | ], 165 | "alphabetize": { 166 | "order": "asc", 167 | "caseInsensitive": true 168 | } 169 | } 170 | ], 171 | "no-duplicate-imports": [ 172 | "error" 173 | ], 174 | "no-shadow": [ 175 | "off" 176 | ], 177 | "@typescript-eslint/no-shadow": [ 178 | "error" 179 | ], 180 | "key-spacing": [ 181 | "error" 182 | ], 183 | "no-multiple-empty-lines": [ 184 | "error" 185 | ], 186 | "@typescript-eslint/no-floating-promises": [ 187 | "error" 188 | ], 189 | "no-return-await": [ 190 | "off" 191 | ], 192 | "@typescript-eslint/return-await": [ 193 | "error" 194 | ], 195 | "no-trailing-spaces": [ 196 | "error" 197 | ], 198 | "dot-notation": [ 199 | "error" 200 | ], 201 | "no-bitwise": [ 202 | "error" 203 | ], 204 | "@typescript-eslint/member-ordering": [ 205 | "error", 206 | { 207 | "default": [ 208 | "public-static-field", 209 | "public-static-method", 210 | "protected-static-field", 211 | "protected-static-method", 212 | "private-static-field", 213 | "private-static-method", 214 | "field", 215 | "constructor", 216 | "method" 217 | ] 218 | } 219 | ] 220 | }, 221 | "overrides": [ 222 | { 223 | "files": [ 224 | ".projenrc.js" 225 | ], 226 | "rules": { 227 | "@typescript-eslint/no-require-imports": "off", 228 | "import/no-extraneous-dependencies": "off" 229 | } 230 | } 231 | ] 232 | } 233 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | *.snap linguist-generated 4 | /.eslintrc.json linguist-generated 5 | /.gitattributes linguist-generated 6 | /.github/pull_request_template.md linguist-generated 7 | /.github/workflows/auto-approve.yml linguist-generated 8 | /.github/workflows/build-sample.yml linguist-generated 9 | /.github/workflows/build.yml linguist-generated 10 | /.github/workflows/pull-request-lint.yml linguist-generated 11 | /.github/workflows/release-v1-main.yml linguist-generated 12 | /.github/workflows/release.yml linguist-generated 13 | /.github/workflows/upgrade-main.yml linguist-generated 14 | /.github/workflows/upgrade-v1-main.yml linguist-generated 15 | /.gitignore linguist-generated 16 | /.mergify.yml linguist-generated 17 | /.npmignore linguist-generated 18 | /.projen/** linguist-generated 19 | /.projen/deps.json linguist-generated 20 | /.projen/files.json linguist-generated 21 | /.projen/tasks.json linguist-generated 22 | /API.md linguist-generated 23 | /cdk.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.js 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 == 'dependabot[bot]') 18 | steps: 19 | - uses: hmarr/auto-approve-action@v2.2.1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/build-sample.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: build-sample 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build-sample: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v1 15 | with: 16 | node-version: "14" 17 | - run: cd sample && yarn && yarn test 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: 14.17.0 26 | - name: Install dependencies 27 | run: yarn install --check-files 28 | - name: build 29 | run: npx projen build 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v3 32 | with: 33 | directory: coverage 34 | - name: Find mutations 35 | id: self_mutation 36 | run: |- 37 | git add . 38 | git diff --staged --patch --exit-code > .repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 39 | - name: Upload patch 40 | if: steps.self_mutation.outputs.self_mutation_happened 41 | uses: actions/upload-artifact@v3 42 | with: 43 | name: .repo.patch 44 | path: .repo.patch 45 | - name: Fail build on mutation 46 | if: steps.self_mutation.outputs.self_mutation_happened 47 | run: |- 48 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 49 | cat .repo.patch 50 | exit 1 51 | - name: Backup artifact permissions 52 | run: cd dist && getfacl -R . > permissions-backup.acl 53 | continue-on-error: true 54 | - name: Upload artifact 55 | uses: actions/upload-artifact@v3 56 | with: 57 | name: build-artifact 58 | path: dist 59 | self-mutation: 60 | needs: build 61 | runs-on: ubuntu-latest 62 | permissions: 63 | contents: write 64 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 65 | steps: 66 | - name: Checkout 67 | uses: actions/checkout@v3 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | ref: ${{ github.event.pull_request.head.ref }} 71 | repository: ${{ github.event.pull_request.head.repo.full_name }} 72 | - name: Download patch 73 | uses: actions/download-artifact@v3 74 | with: 75 | name: .repo.patch 76 | path: ${{ runner.temp }} 77 | - name: Apply patch 78 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 79 | - name: Set git identity 80 | run: |- 81 | git config user.name "github-actions" 82 | git config user.email "github-actions@github.com" 83 | - name: Push changes 84 | run: |2- 85 | git add . 86 | git commit -s -m "chore: self mutation" 87 | git push origin HEAD:${{ github.event.pull_request.head.ref }} 88 | package-js: 89 | needs: build 90 | runs-on: ubuntu-latest 91 | permissions: {} 92 | if: "! needs.build.outputs.self_mutation_happened" 93 | steps: 94 | - uses: actions/setup-node@v3 95 | with: 96 | node-version: 14.17.0 97 | - name: Download build artifacts 98 | uses: actions/download-artifact@v3 99 | with: 100 | name: build-artifact 101 | path: dist 102 | - name: Restore build artifact permissions 103 | run: cd dist && setfacl --restore=permissions-backup.acl 104 | continue-on-error: true 105 | - name: Prepare Repository 106 | run: mv dist .repo 107 | - name: Install Dependencies 108 | run: cd .repo && yarn install --check-files --frozen-lockfile 109 | - name: Create js artifact 110 | run: cd .repo && npx projen package:js 111 | - name: Collect js Artifact 112 | run: mv .repo/dist dist 113 | package-python: 114 | needs: build 115 | runs-on: ubuntu-latest 116 | permissions: {} 117 | if: "! needs.build.outputs.self_mutation_happened" 118 | steps: 119 | - uses: actions/setup-node@v3 120 | with: 121 | node-version: 14.17.0 122 | - uses: actions/setup-python@v4 123 | with: 124 | python-version: 3.x 125 | - name: Download build artifacts 126 | uses: actions/download-artifact@v3 127 | with: 128 | name: build-artifact 129 | path: dist 130 | - name: Restore build artifact permissions 131 | run: cd dist && setfacl --restore=permissions-backup.acl 132 | continue-on-error: true 133 | - name: Prepare Repository 134 | run: mv dist .repo 135 | - name: Install Dependencies 136 | run: cd .repo && yarn install --check-files --frozen-lockfile 137 | - name: Create python artifact 138 | run: cd .repo && npx projen package:python 139 | - name: Collect python Artifact 140 | run: mv .repo/dist dist 141 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js 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 | jobs: 14 | validate: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | permissions: 18 | pull-requests: write 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v5.0.2 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | types: |- 25 | feat 26 | fix 27 | chore 28 | requireScope: false 29 | -------------------------------------------------------------------------------- /.github/workflows/release-v1-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: release-v1-main 4 | on: 5 | push: 6 | branches: 7 | - v1-main 8 | workflow_dispatch: {} 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | outputs: 15 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 16 | env: 17 | CI: "true" 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - name: Set git identity 24 | run: |- 25 | git config user.name "github-actions" 26 | git config user.email "github-actions@github.com" 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 14.17.0 31 | - name: Install dependencies 32 | run: yarn install --check-files --frozen-lockfile 33 | - name: release:v1-main 34 | run: npx projen release:v1-main 35 | - name: Upload coverage to Codecov 36 | uses: codecov/codecov-action@v3 37 | with: 38 | directory: coverage 39 | - name: Check for new commits 40 | id: git_remote 41 | run: echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 42 | - name: Backup artifact permissions 43 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 44 | run: cd dist && getfacl -R . > permissions-backup.acl 45 | continue-on-error: true 46 | - name: Upload artifact 47 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 48 | uses: actions/upload-artifact@v3 49 | with: 50 | name: build-artifact 51 | path: dist 52 | release_github: 53 | name: Publish to GitHub Releases 54 | needs: release 55 | runs-on: ubuntu-latest 56 | permissions: 57 | contents: write 58 | if: needs.release.outputs.latest_commit == github.sha 59 | steps: 60 | - uses: actions/setup-node@v3 61 | with: 62 | node-version: 14.17.0 63 | - name: Download build artifacts 64 | uses: actions/download-artifact@v3 65 | with: 66 | name: build-artifact 67 | path: dist 68 | - name: Restore build artifact permissions 69 | run: cd dist && setfacl --restore=permissions-backup.acl 70 | continue-on-error: true 71 | - name: Prepare Repository 72 | run: mv dist .repo 73 | - name: Collect GitHub Metadata 74 | run: mv .repo/dist dist 75 | - name: Release 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | GITHUB_REPOSITORY: ${{ github.repository }} 79 | GITHUB_REF: ${{ github.ref }} 80 | 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 81 | release_npm: 82 | name: Publish to npm 83 | needs: release 84 | runs-on: ubuntu-latest 85 | permissions: 86 | contents: read 87 | if: needs.release.outputs.latest_commit == github.sha 88 | steps: 89 | - uses: actions/setup-node@v3 90 | with: 91 | node-version: 14.17.0 92 | - name: Download build artifacts 93 | uses: actions/download-artifact@v3 94 | with: 95 | name: build-artifact 96 | path: dist 97 | - name: Restore build artifact permissions 98 | run: cd dist && setfacl --restore=permissions-backup.acl 99 | continue-on-error: true 100 | - name: Prepare Repository 101 | run: mv dist .repo 102 | - name: Install Dependencies 103 | run: cd .repo && yarn install --check-files --frozen-lockfile 104 | - name: Create js artifact 105 | run: cd .repo && npx projen package:js 106 | - name: Collect js Artifact 107 | run: mv .repo/dist dist 108 | - name: Release 109 | env: 110 | NPM_DIST_TAG: latest 111 | NPM_REGISTRY: registry.npmjs.org 112 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 113 | run: npx -p publib@latest publib-npm 114 | release_pypi: 115 | name: Publish to PyPI 116 | needs: release 117 | runs-on: ubuntu-latest 118 | permissions: 119 | contents: read 120 | if: needs.release.outputs.latest_commit == github.sha 121 | steps: 122 | - uses: actions/setup-node@v3 123 | with: 124 | node-version: 14.17.0 125 | - uses: actions/setup-python@v4 126 | with: 127 | python-version: 3.x 128 | - name: Download build artifacts 129 | uses: actions/download-artifact@v3 130 | with: 131 | name: build-artifact 132 | path: dist 133 | - name: Restore build artifact permissions 134 | run: cd dist && setfacl --restore=permissions-backup.acl 135 | continue-on-error: true 136 | - name: Prepare Repository 137 | run: mv dist .repo 138 | - name: Install Dependencies 139 | run: cd .repo && yarn install --check-files --frozen-lockfile 140 | - name: Create python artifact 141 | run: cd .repo && npx projen package:python 142 | - name: Collect python Artifact 143 | run: mv .repo/dist dist 144 | - name: Release 145 | env: 146 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 147 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 148 | run: npx -p publib@latest publib-pypi 149 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | outputs: 15 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 16 | env: 17 | CI: "true" 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | with: 22 | fetch-depth: 0 23 | - name: Set git identity 24 | run: |- 25 | git config user.name "github-actions" 26 | git config user.email "github-actions@github.com" 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v3 29 | with: 30 | node-version: 14.17.0 31 | - name: Install dependencies 32 | run: yarn install --check-files --frozen-lockfile 33 | - name: release 34 | run: npx projen release 35 | - name: Upload coverage to Codecov 36 | uses: codecov/codecov-action@v3 37 | with: 38 | directory: coverage 39 | - name: Check for new commits 40 | id: git_remote 41 | run: echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 42 | - name: Backup artifact permissions 43 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 44 | run: cd dist && getfacl -R . > permissions-backup.acl 45 | continue-on-error: true 46 | - name: Upload artifact 47 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 48 | uses: actions/upload-artifact@v3 49 | with: 50 | name: build-artifact 51 | path: dist 52 | release_github: 53 | name: Publish to GitHub Releases 54 | needs: release 55 | runs-on: ubuntu-latest 56 | permissions: 57 | contents: write 58 | if: needs.release.outputs.latest_commit == github.sha 59 | steps: 60 | - uses: actions/setup-node@v3 61 | with: 62 | node-version: 14.17.0 63 | - name: Download build artifacts 64 | uses: actions/download-artifact@v3 65 | with: 66 | name: build-artifact 67 | path: dist 68 | - name: Restore build artifact permissions 69 | run: cd dist && setfacl --restore=permissions-backup.acl 70 | continue-on-error: true 71 | - name: Prepare Repository 72 | run: mv dist .repo 73 | - name: Collect GitHub Metadata 74 | run: mv .repo/dist dist 75 | - name: Release 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | GITHUB_REPOSITORY: ${{ github.repository }} 79 | GITHUB_REF: ${{ github.ref }} 80 | 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 81 | release_npm: 82 | name: Publish to npm 83 | needs: release 84 | runs-on: ubuntu-latest 85 | permissions: 86 | contents: read 87 | if: needs.release.outputs.latest_commit == github.sha 88 | steps: 89 | - uses: actions/setup-node@v3 90 | with: 91 | node-version: 14.17.0 92 | - name: Download build artifacts 93 | uses: actions/download-artifact@v3 94 | with: 95 | name: build-artifact 96 | path: dist 97 | - name: Restore build artifact permissions 98 | run: cd dist && setfacl --restore=permissions-backup.acl 99 | continue-on-error: true 100 | - name: Prepare Repository 101 | run: mv dist .repo 102 | - name: Install Dependencies 103 | run: cd .repo && yarn install --check-files --frozen-lockfile 104 | - name: Create js artifact 105 | run: cd .repo && npx projen package:js 106 | - name: Collect js Artifact 107 | run: mv .repo/dist dist 108 | - name: Release 109 | env: 110 | NPM_DIST_TAG: latest 111 | NPM_REGISTRY: registry.npmjs.org 112 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 113 | run: npx -p publib@latest publib-npm 114 | release_pypi: 115 | name: Publish to PyPI 116 | needs: release 117 | runs-on: ubuntu-latest 118 | permissions: 119 | contents: read 120 | if: needs.release.outputs.latest_commit == github.sha 121 | steps: 122 | - uses: actions/setup-node@v3 123 | with: 124 | node-version: 14.17.0 125 | - uses: actions/setup-python@v4 126 | with: 127 | python-version: 3.x 128 | - name: Download build artifacts 129 | uses: actions/download-artifact@v3 130 | with: 131 | name: build-artifact 132 | path: dist 133 | - name: Restore build artifact permissions 134 | run: cd dist && setfacl --restore=permissions-backup.acl 135 | continue-on-error: true 136 | - name: Prepare Repository 137 | run: mv dist .repo 138 | - name: Install Dependencies 139 | run: cd .repo && yarn install --check-files --frozen-lockfile 140 | - name: Create python artifact 141 | run: cd .repo && npx projen package:python 142 | - name: Collect python Artifact 143 | run: mv .repo/dist dist 144 | - name: Release 145 | env: 146 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 147 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 148 | run: npx -p publib@latest publib-pypi 149 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: upgrade-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@v3 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 14.17.0 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 | - name: Upload patch 35 | if: steps.create_patch.outputs.patch_created 36 | uses: actions/upload-artifact@v3 37 | with: 38 | name: .repo.patch 39 | path: .repo.patch 40 | pr: 41 | name: Create Pull Request 42 | needs: upgrade 43 | runs-on: ubuntu-latest 44 | permissions: 45 | contents: write 46 | pull-requests: write 47 | if: ${{ needs.upgrade.outputs.patch_created }} 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v3 51 | with: 52 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v3 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@v3 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | chore(deps): upgrade dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: https://github.com/${{ 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: "chore(deps): upgrade dependencies" 82 | labels: auto-approve 83 | body: |- 84 | Upgrades project dependencies. See details in [workflow run]. 85 | 86 | [Workflow Run]: https://github.com/${{ 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 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-v1-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: upgrade-v1-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@v3 19 | with: 20 | ref: v1-main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 14.17.0 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 | - name: Upload patch 35 | if: steps.create_patch.outputs.patch_created 36 | uses: actions/upload-artifact@v3 37 | with: 38 | name: .repo.patch 39 | path: .repo.patch 40 | pr: 41 | name: Create Pull Request 42 | needs: upgrade 43 | runs-on: ubuntu-latest 44 | permissions: 45 | contents: write 46 | pull-requests: write 47 | if: ${{ needs.upgrade.outputs.patch_created }} 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v3 51 | with: 52 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 53 | ref: v1-main 54 | - name: Download patch 55 | uses: actions/download-artifact@v3 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@v3 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | chore(deps): upgrade dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-v1-main" workflow* 80 | branch: github-actions/upgrade-v1-main 81 | title: "chore(deps): upgrade dependencies" 82 | labels: auto-approve 83 | body: |- 84 | Upgrades project dependencies. See details in [workflow run]. 85 | 86 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 87 | 88 | ------ 89 | 90 | *Automatically created by projen via the "upgrade-v1-main" workflow* 91 | author: github-actions 92 | committer: github-actions 93 | signoff: true 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js 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 | cdk.out/ 35 | /sample/lib 36 | !/sample/tsconfig.json 37 | !/.projenrc.js 38 | /test-reports/ 39 | junit.xml 40 | /coverage/ 41 | !/.github/workflows/build.yml 42 | /dist/changelog.md 43 | /dist/version.txt 44 | !/.github/workflows/release.yml 45 | !/.github/workflows/release-v1-main.yml 46 | !/.mergify.yml 47 | !/.github/workflows/upgrade-main.yml 48 | !/.github/workflows/upgrade-v1-main.yml 49 | !/.github/pull_request_template.md 50 | !/test/ 51 | !/tsconfig.dev.json 52 | !/src/ 53 | /lib 54 | /dist/ 55 | !/.eslintrc.json 56 | .jsii 57 | tsconfig.json 58 | !/API.md 59 | !/.github/workflows/build-sample.yml 60 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | conditions: 6 | - "#approved-reviews-by>=1" 7 | - -label~=(do-not-merge) 8 | - status-success=build 9 | - status-success=package-js 10 | - status-success=package-python 11 | pull_request_rules: 12 | - name: Automatic merge on approval and successful build 13 | actions: 14 | delete_head_branch: {} 15 | queue: 16 | method: squash 17 | name: default 18 | commit_message_template: |- 19 | {{ title }} (#{{ number }}) 20 | 21 | {{ body }} 22 | conditions: 23 | - "#approved-reviews-by>=1" 24 | - -label~=(do-not-merge) 25 | - status-success=build 26 | - status-success=package-js 27 | - status-success=package-python 28 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | sample/ 3 | scripts/ 4 | /.projen/ 5 | /test-reports/ 6 | junit.xml 7 | /coverage/ 8 | permissions-backup.acl 9 | /dist/changelog.md 10 | /dist/version.txt 11 | /.mergify.yml 12 | /test/ 13 | /tsconfig.dev.json 14 | /src/ 15 | !/lib/ 16 | !/lib/**/*.js 17 | !/lib/**/*.d.ts 18 | dist 19 | /tsconfig.json 20 | /.github/ 21 | /.vscode/ 22 | /.idea/ 23 | /.projenrc.js 24 | tsconfig.tsbuildinfo 25 | /.eslintrc.json 26 | !.jsii 27 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@types/jest", 5 | "version": "^27", 6 | "type": "build" 7 | }, 8 | { 9 | "name": "@types/node", 10 | "version": "^14", 11 | "type": "build" 12 | }, 13 | { 14 | "name": "@typescript-eslint/eslint-plugin", 15 | "version": "^5", 16 | "type": "build" 17 | }, 18 | { 19 | "name": "@typescript-eslint/parser", 20 | "version": "^5", 21 | "type": "build" 22 | }, 23 | { 24 | "name": "ansi-regex", 25 | "version": "6.0.1", 26 | "type": "build" 27 | }, 28 | { 29 | "name": "eslint-import-resolver-node", 30 | "type": "build" 31 | }, 32 | { 33 | "name": "eslint-import-resolver-typescript", 34 | "type": "build" 35 | }, 36 | { 37 | "name": "eslint-plugin-import", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "eslint", 42 | "version": "^8", 43 | "type": "build" 44 | }, 45 | { 46 | "name": "jest-junit", 47 | "version": "^13", 48 | "type": "build" 49 | }, 50 | { 51 | "name": "jest", 52 | "version": "^27", 53 | "type": "build" 54 | }, 55 | { 56 | "name": "jsii", 57 | "type": "build" 58 | }, 59 | { 60 | "name": "jsii-diff", 61 | "type": "build" 62 | }, 63 | { 64 | "name": "jsii-docgen", 65 | "type": "build" 66 | }, 67 | { 68 | "name": "jsii-pacmak", 69 | "type": "build" 70 | }, 71 | { 72 | "name": "json-schema", 73 | "type": "build" 74 | }, 75 | { 76 | "name": "npm-check-updates", 77 | "version": "^16", 78 | "type": "build" 79 | }, 80 | { 81 | "name": "projen", 82 | "type": "build" 83 | }, 84 | { 85 | "name": "standard-version", 86 | "version": "^9", 87 | "type": "build" 88 | }, 89 | { 90 | "name": "ts-jest", 91 | "version": "^27", 92 | "type": "build" 93 | }, 94 | { 95 | "name": "typescript", 96 | "type": "build" 97 | }, 98 | { 99 | "name": "@types/babel__traverse", 100 | "version": "7.18.2", 101 | "type": "override" 102 | }, 103 | { 104 | "name": "@types/prettier", 105 | "version": "2.6.0", 106 | "type": "override" 107 | }, 108 | { 109 | "name": "aws-cdk-lib", 110 | "version": "^2", 111 | "type": "peer" 112 | }, 113 | { 114 | "name": "constructs", 115 | "version": "^10.0.5", 116 | "type": "peer" 117 | }, 118 | { 119 | "name": "@aws-cdk/aws-batch-alpha", 120 | "version": "^2.8.0-alpha.0", 121 | "type": "runtime" 122 | }, 123 | { 124 | "name": "aws-cdk-lib", 125 | "version": "^2", 126 | "type": "runtime" 127 | }, 128 | { 129 | "name": "constructs", 130 | "version": "^10.0.5", 131 | "type": "runtime" 132 | } 133 | ], 134 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 135 | } 136 | -------------------------------------------------------------------------------- /.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/build-sample.yml", 8 | ".github/workflows/build.yml", 9 | ".github/workflows/pull-request-lint.yml", 10 | ".github/workflows/release-v1-main.yml", 11 | ".github/workflows/release.yml", 12 | ".github/workflows/upgrade-main.yml", 13 | ".github/workflows/upgrade-v1-main.yml", 14 | ".gitignore", 15 | ".mergify.yml", 16 | ".projen/deps.json", 17 | ".projen/files.json", 18 | ".projen/tasks.json", 19 | "LICENSE", 20 | "tsconfig.dev.json" 21 | ], 22 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 23 | } 24 | -------------------------------------------------------------------------------- /.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 | }, 37 | "steps": [ 38 | { 39 | "builtin": "release/bump-version" 40 | } 41 | ], 42 | "condition": "! git log --oneline -1 | grep -q \"chore(release):\"" 43 | }, 44 | "clobber": { 45 | "name": "clobber", 46 | "description": "hard resets to HEAD of origin and cleans the local repo", 47 | "env": { 48 | "BRANCH": "$(git branch --show-current)" 49 | }, 50 | "steps": [ 51 | { 52 | "exec": "git checkout -b scratch", 53 | "name": "save current HEAD in \"scratch\" branch" 54 | }, 55 | { 56 | "exec": "git checkout $BRANCH" 57 | }, 58 | { 59 | "exec": "git fetch origin", 60 | "name": "fetch latest changes from origin" 61 | }, 62 | { 63 | "exec": "git reset --hard origin/$BRANCH", 64 | "name": "hard reset to origin commit" 65 | }, 66 | { 67 | "exec": "git clean -fdx", 68 | "name": "clean all untracked files" 69 | }, 70 | { 71 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 72 | } 73 | ], 74 | "condition": "git diff --exit-code > /dev/null" 75 | }, 76 | "compat": { 77 | "name": "compat", 78 | "description": "Perform API compatibility check against latest version", 79 | "steps": [ 80 | { 81 | "exec": "jsii-diff npm:$(node -p \"require('./package.json').name\") -k --ignore-file .compatignore || (echo \"\nUNEXPECTED BREAKING CHANGES: add keys such as 'removed:constructs.Node.of' to .compatignore to skip.\n\" && exit 1)" 82 | } 83 | ] 84 | }, 85 | "compile": { 86 | "name": "compile", 87 | "description": "Only compile", 88 | "steps": [ 89 | { 90 | "exec": "jsii --silence-warnings=reserved-word" 91 | } 92 | ] 93 | }, 94 | "default": { 95 | "name": "default", 96 | "description": "Synthesize project files", 97 | "steps": [ 98 | { 99 | "exec": "node .projenrc.js" 100 | } 101 | ] 102 | }, 103 | "docgen": { 104 | "name": "docgen", 105 | "description": "Generate API.md from .jsii manifest", 106 | "steps": [ 107 | { 108 | "exec": "jsii-docgen -o API.md" 109 | } 110 | ] 111 | }, 112 | "eject": { 113 | "name": "eject", 114 | "description": "Remove projen from the project", 115 | "env": { 116 | "PROJEN_EJECTING": "true" 117 | }, 118 | "steps": [ 119 | { 120 | "spawn": "default" 121 | } 122 | ] 123 | }, 124 | "eslint": { 125 | "name": "eslint", 126 | "description": "Runs eslint against the codebase", 127 | "steps": [ 128 | { 129 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test build-tools .projenrc.js" 130 | } 131 | ] 132 | }, 133 | "package": { 134 | "name": "package", 135 | "description": "Creates the distribution package", 136 | "steps": [ 137 | { 138 | "exec": "if [ ! -z ${CI} ]; then rsync -a . .repo --exclude .git --exclude node_modules && rm -rf dist && mv .repo dist; else npx projen package-all; fi" 139 | } 140 | ] 141 | }, 142 | "package-all": { 143 | "name": "package-all", 144 | "description": "Packages artifacts for all target languages", 145 | "steps": [ 146 | { 147 | "spawn": "package:js" 148 | }, 149 | { 150 | "spawn": "package:python" 151 | } 152 | ] 153 | }, 154 | "package:js": { 155 | "name": "package:js", 156 | "description": "Create js language bindings", 157 | "steps": [ 158 | { 159 | "exec": "jsii-pacmak -v --target js" 160 | } 161 | ] 162 | }, 163 | "package:python": { 164 | "name": "package:python", 165 | "description": "Create python language bindings", 166 | "steps": [ 167 | { 168 | "exec": "jsii-pacmak -v --target python" 169 | } 170 | ] 171 | }, 172 | "post-compile": { 173 | "name": "post-compile", 174 | "description": "Runs after successful compilation", 175 | "steps": [ 176 | { 177 | "spawn": "docgen" 178 | }, 179 | { 180 | "spawn": "readme" 181 | } 182 | ] 183 | }, 184 | "post-upgrade": { 185 | "name": "post-upgrade", 186 | "description": "Runs after upgrading dependencies" 187 | }, 188 | "pre-compile": { 189 | "name": "pre-compile", 190 | "description": "Prepare the project for compilation" 191 | }, 192 | "readme": { 193 | "name": "readme", 194 | "steps": [ 195 | { 196 | "exec": "./scripts/extractdoc.py \"sample/src/*.ts\" README.md" 197 | } 198 | ] 199 | }, 200 | "release": { 201 | "name": "release", 202 | "description": "Prepare a release from \"main\" branch", 203 | "env": { 204 | "RELEASE": "true", 205 | "MAJOR": "2" 206 | }, 207 | "steps": [ 208 | { 209 | "exec": "rm -fr dist" 210 | }, 211 | { 212 | "spawn": "bump" 213 | }, 214 | { 215 | "spawn": "build" 216 | }, 217 | { 218 | "spawn": "unbump" 219 | }, 220 | { 221 | "exec": "git diff --ignore-space-at-eol --exit-code" 222 | } 223 | ] 224 | }, 225 | "release:v1-main": { 226 | "name": "release:v1-main", 227 | "description": "Prepare a release from \"v1-main\" branch", 228 | "env": { 229 | "RELEASE": "true", 230 | "MAJOR": "1" 231 | }, 232 | "steps": [ 233 | { 234 | "exec": "rm -fr dist" 235 | }, 236 | { 237 | "spawn": "bump" 238 | }, 239 | { 240 | "spawn": "build" 241 | }, 242 | { 243 | "spawn": "unbump" 244 | }, 245 | { 246 | "exec": "git diff --ignore-space-at-eol --exit-code" 247 | } 248 | ] 249 | }, 250 | "test": { 251 | "name": "test", 252 | "description": "Run tests", 253 | "steps": [ 254 | { 255 | "exec": "jest --passWithNoTests --coverageProvider=v8 --updateSnapshot", 256 | "receiveArgs": true 257 | }, 258 | { 259 | "spawn": "eslint" 260 | } 261 | ] 262 | }, 263 | "test:watch": { 264 | "name": "test:watch", 265 | "description": "Run jest in watch mode", 266 | "steps": [ 267 | { 268 | "exec": "jest --watch" 269 | } 270 | ] 271 | }, 272 | "unbump": { 273 | "name": "unbump", 274 | "description": "Restores version to 0.0.0", 275 | "env": { 276 | "OUTFILE": "package.json", 277 | "CHANGELOG": "dist/changelog.md", 278 | "BUMPFILE": "dist/version.txt", 279 | "RELEASETAG": "dist/releasetag.txt", 280 | "RELEASE_TAG_PREFIX": "" 281 | }, 282 | "steps": [ 283 | { 284 | "builtin": "release/reset-version" 285 | } 286 | ] 287 | }, 288 | "upgrade": { 289 | "name": "upgrade", 290 | "description": "upgrade dependencies", 291 | "env": { 292 | "CI": "0" 293 | }, 294 | "steps": [ 295 | { 296 | "exec": "yarn upgrade npm-check-updates" 297 | }, 298 | { 299 | "exec": "npm-check-updates --dep dev --upgrade --target=minor --reject='ansi-regex'" 300 | }, 301 | { 302 | "exec": "npm-check-updates --dep optional --upgrade --target=minor --reject='ansi-regex'" 303 | }, 304 | { 305 | "exec": "npm-check-updates --dep peer --upgrade --target=minor --reject='ansi-regex'" 306 | }, 307 | { 308 | "exec": "npm-check-updates --dep prod --upgrade --target=minor --reject='ansi-regex'" 309 | }, 310 | { 311 | "exec": "npm-check-updates --dep bundle --upgrade --target=minor --reject='ansi-regex'" 312 | }, 313 | { 314 | "exec": "yarn install --check-files" 315 | }, 316 | { 317 | "exec": "yarn upgrade" 318 | }, 319 | { 320 | "exec": "npx projen" 321 | }, 322 | { 323 | "spawn": "post-upgrade" 324 | } 325 | ] 326 | }, 327 | "watch": { 328 | "name": "watch", 329 | "description": "Watch & compile in the background", 330 | "steps": [ 331 | { 332 | "exec": "jsii -w --silence-warnings=reserved-word" 333 | } 334 | ] 335 | } 336 | }, 337 | "env": { 338 | "PATH": "$(npx -c \"node -e \\\"console.log(process.env.PATH)\\\"\")" 339 | }, 340 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 341 | } 342 | -------------------------------------------------------------------------------- /.projenrc.js: -------------------------------------------------------------------------------- 1 | const { awscdk, github, cdk } = require('projen'); 2 | 3 | const project = new cdk.JsiiProject({ 4 | description: 'Generate directly usable AWS CloudFormation template with aws-cdk v2.', 5 | author: 'wchaws', 6 | authorOrganization: true, 7 | repository: 'https://github.com/aws-samples/cdk-bootstrapless-synthesizer.git', 8 | keywords: ['cdk', 'cloudformation', 'aws', 'synthesizer'], 9 | name: 'cdk-bootstrapless-synthesizer', 10 | codeCov: true, 11 | autoApproveOptions: { 12 | secret: 'GITHUB_TOKEN', 13 | allowedUsernames: ['dependabot[bot]'], 14 | }, 15 | autoApproveUpgrades: true, 16 | depsUpgrade: true, 17 | gitignore: [ 18 | 'cdk.out/', 19 | '/sample/lib', 20 | '!/sample/tsconfig.json', 21 | ], 22 | npmignore: [ 23 | 'sample/', 24 | 'scripts/', 25 | ], 26 | releaseEveryCommit: true, 27 | defaultReleaseBranch: 'main', 28 | majorVersion: 2, 29 | releaseBranches: { 30 | 'v1-main': { 31 | majorVersion: 1, 32 | }, 33 | }, 34 | deps: [ 35 | 'aws-cdk-lib@^2', 36 | 'constructs@^10.0.5', 37 | '@aws-cdk/aws-batch-alpha@^2.8.0-alpha.0', 38 | ], 39 | peerDeps: [ 40 | 'aws-cdk-lib@^2', 41 | 'constructs@^10.0.5', 42 | ], 43 | devDeps: [ 44 | 'ansi-regex@6.0.1', 45 | ], 46 | publishToPypi: { 47 | distName: 'cdk-bootstrapless-synthesizer', 48 | module: 'cdk_bootstrapless_synthesizer', 49 | }, 50 | jestOptions: { 51 | jestConfig: { 52 | testPathIgnorePatterns: [ 53 | 'sample/', // https://github.com/projen/projen/issues/1059 54 | 'node_modules/', 55 | ], 56 | }, 57 | }, 58 | minNodeVersion: '14.17.0', 59 | }); 60 | 61 | const sampleProject = new awscdk.AwsCdkTypeScriptApp({ 62 | parent: project, 63 | outdir: 'sample', 64 | cdkVersion: '2.3.0', 65 | cdkVersionPinning: false, 66 | defaultReleaseBranch: 'main', 67 | name: 'sample', 68 | licensed: false, 69 | github: false, 70 | projenVersion: '^0.50.31', 71 | featureFlags: false, 72 | deps: [ 73 | 'cdk-bootstrapless-synthesizer@^2', 74 | 'constructs@^10.0.5', 75 | ], /* Runtime dependencies of this module. */ 76 | // description: undefined, /* The description is just a string that helps people understand the purpose of the package. */ 77 | devDeps: [ 78 | 'ansi-regex@6.0.1', 79 | ], /* Build dependencies for this module. */ 80 | // packageName: undefined, /* The "name" in package.json. */ 81 | // release: undefined, /* Add release management to this project. */ 82 | }); 83 | 84 | const sampleProject2 = new awscdk.AwsCdkTypeScriptApp({ 85 | parent: project, 86 | outdir: 'sample-pipeline', 87 | cdkVersion: '2.10.0', 88 | cdkVersionPinning: false, 89 | defaultReleaseBranch: 'main', 90 | appEntrypoint: 'pipeline.ts', 91 | name: 'sample-pipeline', 92 | licensed: false, 93 | github: false, 94 | featureFlags: false, 95 | deps: [ 96 | '@aws-cdk/aws-apigatewayv2-alpha@^2.10.0-alpha.0', 97 | '@aws-cdk/aws-apigatewayv2-integrations-alpha@^2.10.0-alpha.0', 98 | '@aws-lambda-powertools/logger@^0.4.0', 99 | '@types/aws-lambda@^8.10.89', 100 | 'cdk-bootstrapless-synthesizer@^2', 101 | 'constructs@^10.0.5', 102 | ], /* Runtime dependencies of this module. */ 103 | // description: undefined, /* The description is just a string that helps people understand the purpose of the package. */ 104 | devDeps: [], /* Build dependencies for this module. */ 105 | // packageName: undefined, /* The "name" in package.json. */ 106 | // release: undefined, /* Add release management to this project. */ 107 | }); 108 | 109 | const gh = new github.GitHub(project, { 110 | pullRequestLint: false, 111 | }); 112 | const wf = gh.addWorkflow('build-sample'); 113 | wf.on({ 114 | pull_request: {}, 115 | workflow_dispatch: {}, 116 | }); 117 | wf.addJobs({ 118 | 'build-sample': { 119 | runsOn: 'ubuntu-latest', 120 | permissions: { 121 | contents: 'read', 122 | }, 123 | steps: [ 124 | { uses: 'actions/checkout@v2' }, 125 | { 126 | uses: 'actions/setup-node@v1', 127 | with: { 128 | 'node-version': '14', 129 | }, 130 | }, 131 | { run: 'cd sample && yarn && yarn test' }, 132 | ], 133 | }, 134 | }); 135 | 136 | 137 | project.package.addField('resolutions', { 138 | 'trim-newlines': '3.0.1', 139 | }); 140 | sampleProject.package.addField('resolutions', { 141 | minimatch: '3.0.5', 142 | }); 143 | sampleProject2.package.addField('resolutions', { 144 | minimatch: '3.0.5', 145 | }); 146 | 147 | 148 | const readme = project.addTask('readme'); 149 | readme.exec('./scripts/extractdoc.py "sample/src/*.ts" README.md'); 150 | project.postCompileTask.spawn(readme); 151 | 152 | project.synth(); 153 | -------------------------------------------------------------------------------- /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-bootstrapless-synthesizer 2 | 3 | [![npm version](https://img.shields.io/npm/v/cdk-bootstrapless-synthesizer)](https://www.npmjs.com/package/cdk-bootstrapless-synthesizer) 4 | [![PyPI](https://img.shields.io/pypi/v/cdk-bootstrapless-synthesizer)](https://pypi.org/project/cdk-bootstrapless-synthesizer) 5 | [![npm](https://img.shields.io/npm/dw/cdk-bootstrapless-synthesizer?label=npm%20downloads)](https://www.npmjs.com/package/cdk-bootstrapless-synthesizer) 6 | [![PyPI - Downloads](https://img.shields.io/pypi/dw/cdk-bootstrapless-synthesizer?label=pypi%20downloads)](https://pypi.org/project/cdk-bootstrapless-synthesizer) 7 | 8 | A bootstrapless stack synthesizer that is designated to generate templates that can be directly used by AWS CloudFormation. 9 | 10 | Please use ^1.0.0 for cdk version 1.x.x, use ^2.0.0 for cdk version 2.x.x 11 | 12 | ## Usage 13 | ```ts 14 | import { BootstraplessStackSynthesizer } from 'cdk-bootstrapless-synthesizer'; 15 | ``` 16 | [main.ts](sample/src/main.ts) 17 | ```ts 18 | const app = new App(); 19 | 20 | new MyStack(app, 'my-stack-dev', { 21 | synthesizer: new BootstraplessStackSynthesizer({ 22 | templateBucketName: 'cfn-template-bucket', 23 | 24 | fileAssetBucketName: 'file-asset-bucket-${AWS::Region}', 25 | fileAssetRegionSet: ['us-west-1', 'us-west-2'], 26 | fileAssetPrefix: 'file-asset-prefix/latest/', 27 | 28 | imageAssetRepositoryName: 'your-ecr-repo-name', 29 | imageAssetAccountId: '1234567890', 30 | imageAssetTagPrefix: 'latest-', 31 | imageAssetRegionSet: ['us-west-1', 'us-west-2'], 32 | }), 33 | }); 34 | 35 | // Or by environment variables 36 | env.BSS_TEMPLATE_BUCKET_NAME = 'cfn-template-bucket'; 37 | 38 | env.BSS_FILE_ASSET_BUCKET_NAME = 'file-asset-bucket-\${AWS::Region}'; 39 | env.BSS_FILE_ASSET_REGION_SET = 'us-west-1,us-west-2'; 40 | env.BSS_FILE_ASSET_PREFIX = 'file-asset-prefix/latest/'; 41 | 42 | env.BSS_IMAGE_ASSET_REPOSITORY_NAME = 'your-ecr-repo-name'; 43 | env.BSS_IMAGE_ASSET_ACCOUNT_ID = '1234567890'; 44 | env.BSS_IMAGE_ASSET_TAG_PREFIX = 'latest-'; 45 | env.BSS_IMAGE_ASSET_REGION_SET = 'us-west-1,us-west-2'; 46 | 47 | new MyStack(app, 'my-stack-dev2', { 48 | synthesizer: new BootstraplessStackSynthesizer(), 49 | }); 50 | 51 | // use Aspect to grant the role to pull ECR repository from account BSS_IMAGE_ASSET_ACCOUNT_ID 52 | ``` 53 | [main.ts](sample/src/main.ts) 54 | 55 | Synth AWS CloudFormation templates, assets and upload them 56 | 57 | ```shell 58 | $ cdk synth 59 | $ npx cdk-assets publish -p cdk.out/my-stack-dev.assets.json -v 60 | ``` 61 | ## Limitations 62 | When using `BSS_IMAGE_ASSET_ACCOUNT_ID` to push ECR repository to shared account, you need use `Aspect` to grant the role with policy to pull the repository from cross account. Or using the following `WithCrossAccount` techniques. 63 | 64 | Currently only below scenarios are supported, 65 | 66 | - ECS 67 | - SageMaker training job integrated with Step Functions 68 | - AWS Batch 69 | - AWS Lambda 70 | 71 | For other scenarios, the feature request or pull request are welcome. 72 | ```ts 73 | function OverrideRepositoryAccount(scope: Construct, id: string, repo: IRepository): IRepository { 74 | class Import extends RepositoryBase { 75 | public repositoryName = repo.repositoryName; 76 | public repositoryArn = Repository.arnForLocalRepository(repo.repositoryName, scope, env.BSS_IMAGE_ASSET_ACCOUNT_ID); 77 | 78 | public addToResourcePolicy(_statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { 79 | // dropped 80 | return { statementAdded: false }; 81 | } 82 | } 83 | 84 | return new Import(scope, id); 85 | } 86 | 87 | function WithCrossAccount(image: DockerImageAsset): DockerImageAsset { 88 | image.repository = OverrideRepositoryAccount(image, 'CrossAccountRepo', image.repository); 89 | return image; 90 | } 91 | 92 | export class SampleStack extends Stack { 93 | constructor(scope: Construct, id: string, props: StackProps = {}) { 94 | super(scope, id, props); 95 | 96 | const image = WithCrossAccount(new DockerImageAsset(this, 'MyBuildImage', { 97 | directory: path.join(__dirname, '../docker'), 98 | })); 99 | 100 | new CfnOutput(this, 'output', { value: image.imageUri }); 101 | 102 | const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef'); 103 | taskDefinition.addContainer('DefaultContainer', { 104 | image: ecs.ContainerImage.fromDockerImageAsset(image), 105 | memoryLimitMiB: 512, 106 | }); 107 | 108 | fromAsset(this, 'stepfunctions', { 109 | directory: path.join(__dirname, '../docker'), 110 | }); 111 | } 112 | } 113 | ``` 114 | [main.ts](sample/src/main.ts) 115 | ## Sample Project 116 | 117 | See [Sample Project](./sample/README.md) 118 | 119 | ## API Reference 120 | 121 | See [API Reference](./API.md) for API details. 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-bootstrapless-synthesizer", 3 | "description": "Generate directly usable AWS CloudFormation template with aws-cdk v2.", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/aws-samples/cdk-bootstrapless-synthesizer.git" 7 | }, 8 | "scripts": { 9 | "build": "npx projen build", 10 | "bump": "npx projen bump", 11 | "clobber": "npx projen clobber", 12 | "compat": "npx projen compat", 13 | "compile": "npx projen compile", 14 | "default": "npx projen default", 15 | "docgen": "npx projen docgen", 16 | "eject": "npx projen eject", 17 | "eslint": "npx projen eslint", 18 | "package": "npx projen package", 19 | "package-all": "npx projen package-all", 20 | "package:js": "npx projen package:js", 21 | "package:python": "npx projen package:python", 22 | "post-compile": "npx projen post-compile", 23 | "post-upgrade": "npx projen post-upgrade", 24 | "pre-compile": "npx projen pre-compile", 25 | "readme": "npx projen readme", 26 | "release": "npx projen release", 27 | "release:v1-main": "npx projen release:v1-main", 28 | "test": "npx projen test", 29 | "test:watch": "npx projen test:watch", 30 | "unbump": "npx projen unbump", 31 | "upgrade": "npx projen upgrade", 32 | "watch": "npx projen watch", 33 | "projen": "npx projen" 34 | }, 35 | "author": { 36 | "name": "wchaws", 37 | "organization": true 38 | }, 39 | "devDependencies": { 40 | "@types/jest": "^27", 41 | "@types/node": "^14", 42 | "@typescript-eslint/eslint-plugin": "^5", 43 | "@typescript-eslint/parser": "^5", 44 | "ansi-regex": "6.0.1", 45 | "eslint": "^8", 46 | "eslint-import-resolver-node": "^0.3.6", 47 | "eslint-import-resolver-typescript": "^2.7.1", 48 | "eslint-plugin-import": "^2.26.0", 49 | "jest": "^27", 50 | "jest-junit": "^13", 51 | "jsii": "1.72.0", 52 | "jsii-diff": "^1.72.0", 53 | "jsii-docgen": "^4.2.44", 54 | "jsii-pacmak": "^1.72.0", 55 | "json-schema": "^0.4.0", 56 | "npm-check-updates": "^16", 57 | "projen": "^0.65.76", 58 | "standard-version": "^9", 59 | "ts-jest": "^27", 60 | "typescript": "^4.9.4" 61 | }, 62 | "peerDependencies": { 63 | "aws-cdk-lib": "^2", 64 | "constructs": "^10.0.5" 65 | }, 66 | "dependencies": { 67 | "@aws-cdk/aws-batch-alpha": "^2.8.0-alpha.0", 68 | "aws-cdk-lib": "^2", 69 | "constructs": "^10.0.5" 70 | }, 71 | "keywords": [ 72 | "aws", 73 | "cdk", 74 | "cloudformation", 75 | "synthesizer" 76 | ], 77 | "engines": { 78 | "node": ">= 14.17.0" 79 | }, 80 | "main": "lib/index.js", 81 | "license": "Apache-2.0", 82 | "version": "0.0.0", 83 | "jest": { 84 | "testPathIgnorePatterns": [ 85 | "sample/", 86 | "node_modules/" 87 | ], 88 | "testMatch": [ 89 | "/src/**/__tests__/**/*.ts?(x)", 90 | "/(test|src)/**/*(*.)@(spec|test).ts?(x)" 91 | ], 92 | "clearMocks": true, 93 | "collectCoverage": true, 94 | "coverageReporters": [ 95 | "json", 96 | "lcov", 97 | "clover", 98 | "cobertura", 99 | "text" 100 | ], 101 | "coverageDirectory": "coverage", 102 | "coveragePathIgnorePatterns": [ 103 | "sample/", 104 | "node_modules/" 105 | ], 106 | "watchPathIgnorePatterns": [ 107 | "/node_modules/" 108 | ], 109 | "reporters": [ 110 | "default", 111 | [ 112 | "jest-junit", 113 | { 114 | "outputDirectory": "test-reports" 115 | } 116 | ] 117 | ], 118 | "preset": "ts-jest", 119 | "globals": { 120 | "ts-jest": { 121 | "tsconfig": "tsconfig.dev.json" 122 | } 123 | } 124 | }, 125 | "types": "lib/index.d.ts", 126 | "stability": "stable", 127 | "jsii": { 128 | "outdir": "dist", 129 | "targets": { 130 | "python": { 131 | "distName": "cdk-bootstrapless-synthesizer", 132 | "module": "cdk_bootstrapless_synthesizer" 133 | } 134 | }, 135 | "tsc": { 136 | "outDir": "lib", 137 | "rootDir": "src" 138 | } 139 | }, 140 | "resolutions": { 141 | "trim-newlines": "3.0.1", 142 | "@types/prettier": "2.6.0", 143 | "@types/babel__traverse": "7.18.2" 144 | }, 145 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 146 | } 147 | -------------------------------------------------------------------------------- /sample-pipeline/.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js 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 | ], 21 | "settings": { 22 | "import/parsers": { 23 | "@typescript-eslint/parser": [ 24 | ".ts", 25 | ".tsx" 26 | ] 27 | }, 28 | "import/resolver": { 29 | "node": {}, 30 | "typescript": { 31 | "project": "./tsconfig.dev.json", 32 | "alwaysTryTypes": true 33 | } 34 | } 35 | }, 36 | "ignorePatterns": [ 37 | "*.js", 38 | "!.projenrc.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage" 43 | ], 44 | "rules": { 45 | "indent": [ 46 | "off" 47 | ], 48 | "@typescript-eslint/indent": [ 49 | "error", 50 | 2 51 | ], 52 | "quotes": [ 53 | "error", 54 | "single", 55 | { 56 | "avoidEscape": true 57 | } 58 | ], 59 | "comma-dangle": [ 60 | "error", 61 | "always-multiline" 62 | ], 63 | "comma-spacing": [ 64 | "error", 65 | { 66 | "before": false, 67 | "after": true 68 | } 69 | ], 70 | "no-multi-spaces": [ 71 | "error", 72 | { 73 | "ignoreEOLComments": false 74 | } 75 | ], 76 | "array-bracket-spacing": [ 77 | "error", 78 | "never" 79 | ], 80 | "array-bracket-newline": [ 81 | "error", 82 | "consistent" 83 | ], 84 | "object-curly-spacing": [ 85 | "error", 86 | "always" 87 | ], 88 | "object-curly-newline": [ 89 | "error", 90 | { 91 | "multiline": true, 92 | "consistent": true 93 | } 94 | ], 95 | "object-property-newline": [ 96 | "error", 97 | { 98 | "allowAllPropertiesOnSameLine": true 99 | } 100 | ], 101 | "keyword-spacing": [ 102 | "error" 103 | ], 104 | "brace-style": [ 105 | "error", 106 | "1tbs", 107 | { 108 | "allowSingleLine": true 109 | } 110 | ], 111 | "space-before-blocks": [ 112 | "error" 113 | ], 114 | "curly": [ 115 | "error", 116 | "multi-line", 117 | "consistent" 118 | ], 119 | "@typescript-eslint/member-delimiter-style": [ 120 | "error" 121 | ], 122 | "semi": [ 123 | "error", 124 | "always" 125 | ], 126 | "max-len": [ 127 | "error", 128 | { 129 | "code": 150, 130 | "ignoreUrls": true, 131 | "ignoreStrings": true, 132 | "ignoreTemplateLiterals": true, 133 | "ignoreComments": true, 134 | "ignoreRegExpLiterals": true 135 | } 136 | ], 137 | "quote-props": [ 138 | "error", 139 | "consistent-as-needed" 140 | ], 141 | "@typescript-eslint/no-require-imports": [ 142 | "error" 143 | ], 144 | "import/no-extraneous-dependencies": [ 145 | "error", 146 | { 147 | "devDependencies": [ 148 | "**/test/**", 149 | "**/build-tools/**" 150 | ], 151 | "optionalDependencies": false, 152 | "peerDependencies": true 153 | } 154 | ], 155 | "import/no-unresolved": [ 156 | "error" 157 | ], 158 | "import/order": [ 159 | "warn", 160 | { 161 | "groups": [ 162 | "builtin", 163 | "external" 164 | ], 165 | "alphabetize": { 166 | "order": "asc", 167 | "caseInsensitive": true 168 | } 169 | } 170 | ], 171 | "no-duplicate-imports": [ 172 | "error" 173 | ], 174 | "no-shadow": [ 175 | "off" 176 | ], 177 | "@typescript-eslint/no-shadow": [ 178 | "error" 179 | ], 180 | "key-spacing": [ 181 | "error" 182 | ], 183 | "no-multiple-empty-lines": [ 184 | "error" 185 | ], 186 | "@typescript-eslint/no-floating-promises": [ 187 | "error" 188 | ], 189 | "no-return-await": [ 190 | "off" 191 | ], 192 | "@typescript-eslint/return-await": [ 193 | "error" 194 | ], 195 | "no-trailing-spaces": [ 196 | "error" 197 | ], 198 | "dot-notation": [ 199 | "error" 200 | ], 201 | "no-bitwise": [ 202 | "error" 203 | ], 204 | "@typescript-eslint/member-ordering": [ 205 | "error", 206 | { 207 | "default": [ 208 | "public-static-field", 209 | "public-static-method", 210 | "protected-static-field", 211 | "protected-static-method", 212 | "private-static-field", 213 | "private-static-method", 214 | "field", 215 | "constructor", 216 | "method" 217 | ] 218 | } 219 | ] 220 | }, 221 | "overrides": [ 222 | { 223 | "files": [ 224 | ".projenrc.js" 225 | ], 226 | "rules": { 227 | "@typescript-eslint/no-require-imports": "off", 228 | "import/no-extraneous-dependencies": "off" 229 | } 230 | } 231 | ] 232 | } 233 | -------------------------------------------------------------------------------- /sample-pipeline/.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | /.gitattributes linguist-generated 4 | /.projen/** linguist-generated 5 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /sample-pipeline/.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/package.json 7 | !/.npmignore 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | lib-cov 20 | coverage 21 | *.lcov 22 | .nyc_output 23 | build/Release 24 | node_modules/ 25 | jspm_packages/ 26 | *.tsbuildinfo 27 | .eslintcache 28 | *.tgz 29 | .yarn-integrity 30 | .cache 31 | !/.projenrc.js 32 | /test-reports/ 33 | junit.xml 34 | /coverage/ 35 | !/test/ 36 | !/tsconfig.json 37 | !/tsconfig.dev.json 38 | !/src/ 39 | /lib 40 | /dist/ 41 | !/.eslintrc.json 42 | /assets/ 43 | !/cdk.json 44 | /cdk.out/ 45 | .cdk.staging/ 46 | .parcel-cache/ 47 | -------------------------------------------------------------------------------- /sample-pipeline/.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | /test/ 7 | /tsconfig.dev.json 8 | /src/ 9 | !/lib/ 10 | !/lib/**/*.js 11 | !/lib/**/*.d.ts 12 | dist 13 | /tsconfig.json 14 | /.github/ 15 | /.vscode/ 16 | /.idea/ 17 | /.projenrc.js 18 | tsconfig.tsbuildinfo 19 | /.eslintrc.json 20 | !/assets/ 21 | cdk.out/ 22 | .cdk.staging/ 23 | -------------------------------------------------------------------------------- /sample-pipeline/.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@types/jest", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@types/node", 9 | "version": "^14", 10 | "type": "build" 11 | }, 12 | { 13 | "name": "@typescript-eslint/eslint-plugin", 14 | "version": "^5", 15 | "type": "build" 16 | }, 17 | { 18 | "name": "@typescript-eslint/parser", 19 | "version": "^5", 20 | "type": "build" 21 | }, 22 | { 23 | "name": "aws-cdk", 24 | "version": "^2.10.0", 25 | "type": "build" 26 | }, 27 | { 28 | "name": "esbuild", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "eslint-import-resolver-node", 33 | "type": "build" 34 | }, 35 | { 36 | "name": "eslint-import-resolver-typescript", 37 | "type": "build" 38 | }, 39 | { 40 | "name": "eslint-plugin-import", 41 | "type": "build" 42 | }, 43 | { 44 | "name": "eslint", 45 | "version": "^8", 46 | "type": "build" 47 | }, 48 | { 49 | "name": "jest", 50 | "type": "build" 51 | }, 52 | { 53 | "name": "jest-junit", 54 | "version": "^13", 55 | "type": "build" 56 | }, 57 | { 58 | "name": "json-schema", 59 | "type": "build" 60 | }, 61 | { 62 | "name": "npm-check-updates", 63 | "version": "^16", 64 | "type": "build" 65 | }, 66 | { 67 | "name": "projen", 68 | "type": "build" 69 | }, 70 | { 71 | "name": "ts-jest", 72 | "type": "build" 73 | }, 74 | { 75 | "name": "ts-node", 76 | "type": "build" 77 | }, 78 | { 79 | "name": "typescript", 80 | "type": "build" 81 | }, 82 | { 83 | "name": "@aws-cdk/aws-apigatewayv2-alpha", 84 | "version": "^2.10.0-alpha.0", 85 | "type": "runtime" 86 | }, 87 | { 88 | "name": "@aws-cdk/aws-apigatewayv2-integrations-alpha", 89 | "version": "^2.10.0-alpha.0", 90 | "type": "runtime" 91 | }, 92 | { 93 | "name": "@aws-lambda-powertools/logger", 94 | "version": "^0.4.0", 95 | "type": "runtime" 96 | }, 97 | { 98 | "name": "@types/aws-lambda", 99 | "version": "^8.10.89", 100 | "type": "runtime" 101 | }, 102 | { 103 | "name": "aws-cdk-lib", 104 | "version": "^2.10.0", 105 | "type": "runtime" 106 | }, 107 | { 108 | "name": "cdk-bootstrapless-synthesizer", 109 | "version": "^2", 110 | "type": "runtime" 111 | }, 112 | { 113 | "name": "constructs", 114 | "version": "^10.0.5", 115 | "type": "runtime" 116 | } 117 | ], 118 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 119 | } 120 | -------------------------------------------------------------------------------- /sample-pipeline/.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".gitignore", 6 | ".npmignore", 7 | ".projen/deps.json", 8 | ".projen/files.json", 9 | ".projen/tasks.json", 10 | "cdk.json", 11 | "tsconfig.dev.json", 12 | "tsconfig.json" 13 | ], 14 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 15 | } 16 | -------------------------------------------------------------------------------- /sample-pipeline/.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "pre-compile" 9 | }, 10 | { 11 | "spawn": "compile" 12 | }, 13 | { 14 | "spawn": "post-compile" 15 | }, 16 | { 17 | "spawn": "test" 18 | }, 19 | { 20 | "spawn": "package" 21 | } 22 | ] 23 | }, 24 | "bundle": { 25 | "name": "bundle", 26 | "description": "Prepare assets" 27 | }, 28 | "clobber": { 29 | "name": "clobber", 30 | "description": "hard resets to HEAD of origin and cleans the local repo", 31 | "env": { 32 | "BRANCH": "$(git branch --show-current)" 33 | }, 34 | "steps": [ 35 | { 36 | "exec": "git checkout -b scratch", 37 | "name": "save current HEAD in \"scratch\" branch" 38 | }, 39 | { 40 | "exec": "git checkout $BRANCH" 41 | }, 42 | { 43 | "exec": "git fetch origin", 44 | "name": "fetch latest changes from origin" 45 | }, 46 | { 47 | "exec": "git reset --hard origin/$BRANCH", 48 | "name": "hard reset to origin commit" 49 | }, 50 | { 51 | "exec": "git clean -fdx", 52 | "name": "clean all untracked files" 53 | }, 54 | { 55 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 56 | } 57 | ], 58 | "condition": "git diff --exit-code > /dev/null" 59 | }, 60 | "compile": { 61 | "name": "compile", 62 | "description": "Only compile" 63 | }, 64 | "default": { 65 | "name": "default", 66 | "description": "Synthesize project files", 67 | "steps": [ 68 | { 69 | "exec": "node .projenrc.js" 70 | } 71 | ] 72 | }, 73 | "deploy": { 74 | "name": "deploy", 75 | "description": "Deploys your CDK app to the AWS cloud", 76 | "steps": [ 77 | { 78 | "exec": "cdk deploy", 79 | "receiveArgs": true 80 | } 81 | ] 82 | }, 83 | "destroy": { 84 | "name": "destroy", 85 | "description": "Destroys your cdk app in the AWS cloud", 86 | "steps": [ 87 | { 88 | "exec": "cdk destroy", 89 | "receiveArgs": true 90 | } 91 | ] 92 | }, 93 | "diff": { 94 | "name": "diff", 95 | "description": "Diffs the currently deployed app against your code", 96 | "steps": [ 97 | { 98 | "exec": "cdk diff" 99 | } 100 | ] 101 | }, 102 | "eject": { 103 | "name": "eject", 104 | "description": "Remove projen from the project", 105 | "env": { 106 | "PROJEN_EJECTING": "true" 107 | }, 108 | "steps": [ 109 | { 110 | "spawn": "default" 111 | } 112 | ] 113 | }, 114 | "eslint": { 115 | "name": "eslint", 116 | "description": "Runs eslint against the codebase", 117 | "steps": [ 118 | { 119 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test build-tools .projenrc.js" 120 | } 121 | ] 122 | }, 123 | "package": { 124 | "name": "package", 125 | "description": "Creates the distribution package" 126 | }, 127 | "post-compile": { 128 | "name": "post-compile", 129 | "description": "Runs after successful compilation", 130 | "steps": [ 131 | { 132 | "spawn": "synth:silent" 133 | } 134 | ] 135 | }, 136 | "post-upgrade": { 137 | "name": "post-upgrade", 138 | "description": "Runs after upgrading dependencies" 139 | }, 140 | "pre-compile": { 141 | "name": "pre-compile", 142 | "description": "Prepare the project for compilation" 143 | }, 144 | "synth": { 145 | "name": "synth", 146 | "description": "Synthesizes your cdk app into cdk.out", 147 | "steps": [ 148 | { 149 | "exec": "cdk synth" 150 | } 151 | ] 152 | }, 153 | "synth:silent": { 154 | "name": "synth:silent", 155 | "description": "Synthesizes your cdk app into cdk.out and suppresses the template in stdout (part of \"yarn build\")", 156 | "steps": [ 157 | { 158 | "exec": "cdk synth -q" 159 | } 160 | ] 161 | }, 162 | "test": { 163 | "name": "test", 164 | "description": "Run tests", 165 | "steps": [ 166 | { 167 | "exec": "jest --passWithNoTests --updateSnapshot", 168 | "receiveArgs": true 169 | }, 170 | { 171 | "spawn": "eslint" 172 | } 173 | ] 174 | }, 175 | "test:watch": { 176 | "name": "test:watch", 177 | "description": "Run jest in watch mode", 178 | "steps": [ 179 | { 180 | "exec": "jest --watch" 181 | } 182 | ] 183 | }, 184 | "upgrade": { 185 | "name": "upgrade", 186 | "description": "upgrade dependencies", 187 | "env": { 188 | "CI": "0" 189 | }, 190 | "steps": [ 191 | { 192 | "exec": "yarn upgrade npm-check-updates" 193 | }, 194 | { 195 | "exec": "npm-check-updates --dep dev --upgrade --target=minor" 196 | }, 197 | { 198 | "exec": "npm-check-updates --dep optional --upgrade --target=minor" 199 | }, 200 | { 201 | "exec": "npm-check-updates --dep peer --upgrade --target=minor" 202 | }, 203 | { 204 | "exec": "npm-check-updates --dep prod --upgrade --target=minor" 205 | }, 206 | { 207 | "exec": "npm-check-updates --dep bundle --upgrade --target=minor" 208 | }, 209 | { 210 | "exec": "yarn install --check-files" 211 | }, 212 | { 213 | "exec": "yarn upgrade" 214 | }, 215 | { 216 | "exec": "npx projen" 217 | }, 218 | { 219 | "spawn": "post-upgrade" 220 | } 221 | ] 222 | }, 223 | "watch": { 224 | "name": "watch", 225 | "description": "Watches changes in your source code and rebuilds and deploys to the current account", 226 | "steps": [ 227 | { 228 | "exec": "cdk deploy --hotswap" 229 | }, 230 | { 231 | "exec": "cdk watch" 232 | } 233 | ] 234 | } 235 | }, 236 | "env": { 237 | "PATH": "$(npx -c \"node -e \\\"console.log(process.env.PATH)\\\"\")" 238 | }, 239 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 240 | } 241 | -------------------------------------------------------------------------------- /sample-pipeline/README.md: -------------------------------------------------------------------------------- 1 | # Pipeline Example for CDK bootstrapless synthesizer 2 | 3 | It's an example AWS CodePipeline to publish a CDK application with ECS container and API Gateway/Lambda 4 | to **one-click deployable** CloudForamtion templates. 5 | 6 | ## Prerequisites 7 | 8 | - AWS Account with **disable** [Blocking public access to S3 objects][blocking-s3-public-access] 9 | - Store your [Github token in secret manager][github-token] named `github-token` or any custom name 10 | 11 | ## Deploy pipeline 12 | 13 | ```bash 14 | yarn install --check-files --frozen-lockfile 15 | npx cdk deploy 16 | # specify the custom secret name of your github token 17 | npx cdk deploy -c GithubToken= 18 | ``` 19 | 20 | ## Release one-clickable CloudFormation for your CDK application 21 | 22 | Release the pipeline name starting with `PipelineStack-CDKToCloudFormationPublishPipeline` in AWS CodePipeline. 23 | 24 | [blocking-s3-public-access]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html 25 | [github-token]: https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_codepipeline_actions-readme.html#github -------------------------------------------------------------------------------- /sample-pipeline/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node -P tsconfig.json --prefer-ts-exts src/pipeline.ts", 3 | "output": "cdk.out", 4 | "build": "npx projen bundle", 5 | "watch": { 6 | "include": [ 7 | "src/**/*.ts", 8 | "test/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "README.md", 12 | "cdk*.json", 13 | "**/*.d.ts", 14 | "**/*.js", 15 | "tsconfig.json", 16 | "package*.json", 17 | "yarn.lock", 18 | "node_modules" 19 | ] 20 | }, 21 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 22 | } 23 | -------------------------------------------------------------------------------- /sample-pipeline/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample-pipeline", 3 | "scripts": { 4 | "build": "npx projen build", 5 | "bundle": "npx projen bundle", 6 | "clobber": "npx projen clobber", 7 | "compile": "npx projen compile", 8 | "default": "npx projen default", 9 | "deploy": "npx projen deploy", 10 | "destroy": "npx projen destroy", 11 | "diff": "npx projen diff", 12 | "eject": "npx projen eject", 13 | "eslint": "npx projen eslint", 14 | "package": "npx projen package", 15 | "post-compile": "npx projen post-compile", 16 | "post-upgrade": "npx projen post-upgrade", 17 | "pre-compile": "npx projen pre-compile", 18 | "synth": "npx projen synth", 19 | "synth:silent": "npx projen synth:silent", 20 | "test": "npx projen test", 21 | "test:watch": "npx projen test:watch", 22 | "upgrade": "npx projen upgrade", 23 | "watch": "npx projen watch", 24 | "projen": "npx projen" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^27.4.0", 28 | "@types/node": "^14", 29 | "@typescript-eslint/eslint-plugin": "^5", 30 | "@typescript-eslint/parser": "^5", 31 | "aws-cdk": "^2.10.0", 32 | "esbuild": "^0.14.14", 33 | "eslint": "^8", 34 | "eslint-import-resolver-node": "^0.3.6", 35 | "eslint-import-resolver-typescript": "^2.5.0", 36 | "eslint-plugin-import": "^2.25.4", 37 | "jest": "^27.4.7", 38 | "jest-junit": "^13", 39 | "json-schema": "^0.4.0", 40 | "npm-check-updates": "^16", 41 | "projen": "^0.52.8", 42 | "ts-jest": "^27.1.3", 43 | "ts-node": "^9", 44 | "typescript": "^4.5.5" 45 | }, 46 | "dependencies": { 47 | "@aws-cdk/aws-apigatewayv2-alpha": "^2.10.0-alpha.0", 48 | "@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.10.0-alpha.0", 49 | "@aws-lambda-powertools/logger": "^0.4.0", 50 | "@types/aws-lambda": "^8.10.89", 51 | "aws-cdk-lib": "^2.10.0", 52 | "cdk-bootstrapless-synthesizer": "^2", 53 | "constructs": "^10.0.5" 54 | }, 55 | "license": "UNLICENSED", 56 | "version": "0.0.0", 57 | "jest": { 58 | "testMatch": [ 59 | "/src/**/__tests__/**/*.ts?(x)", 60 | "/(test|src)/**/*(*.)@(spec|test).ts?(x)" 61 | ], 62 | "clearMocks": true, 63 | "collectCoverage": true, 64 | "coverageReporters": [ 65 | "json", 66 | "lcov", 67 | "clover", 68 | "cobertura", 69 | "text" 70 | ], 71 | "coverageDirectory": "coverage", 72 | "coveragePathIgnorePatterns": [ 73 | "/node_modules/" 74 | ], 75 | "testPathIgnorePatterns": [ 76 | "/node_modules/" 77 | ], 78 | "watchPathIgnorePatterns": [ 79 | "/node_modules/" 80 | ], 81 | "reporters": [ 82 | "default", 83 | [ 84 | "jest-junit", 85 | { 86 | "outputDirectory": "test-reports" 87 | } 88 | ] 89 | ], 90 | "preset": "ts-jest", 91 | "globals": { 92 | "ts-jest": { 93 | "tsconfig": "tsconfig.dev.json" 94 | } 95 | } 96 | }, 97 | "resolutions": { 98 | "minimatch": "3.0.5" 99 | }, 100 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 101 | } 102 | -------------------------------------------------------------------------------- /sample-pipeline/src/app.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { HttpApi, HttpMethod, PayloadFormatVersion } from '@aws-cdk/aws-apigatewayv2-alpha'; 3 | import { HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations-alpha'; 4 | import { Stack, StackProps, CfnOutput, App, Aspects, Duration } from 'aws-cdk-lib'; 5 | import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; 6 | import { ContainerImage } from 'aws-cdk-lib/aws-ecs'; 7 | import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns'; 8 | import { Architecture, Runtime } from 'aws-cdk-lib/aws-lambda'; 9 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; 10 | import { BootstraplessStackSynthesizer, CompositeECRRepositoryAspect } from 'cdk-bootstrapless-synthesizer'; 11 | import { Construct } from 'constructs'; 12 | 13 | export class AppStack extends Stack { 14 | constructor(scope: Construct, id: string, props: StackProps = {}) { 15 | super(scope, id, props); 16 | 17 | new ApplicationLoadBalancedFargateService(this, 'Service', { 18 | memoryLimitMiB: 512, 19 | cpu: 256, 20 | taskImageOptions: { 21 | image: ContainerImage.fromDockerImageAsset(new DockerImageAsset(this, 'BulkLoadGraphDataImage', { 22 | directory: path.join(__dirname, './app'), 23 | })), 24 | }, 25 | }); 26 | 27 | const echoFunc = new NodejsFunction(this, 'echo', { 28 | entry: path.join(__dirname, './lambda/index.ts'), 29 | handler: 'handler', 30 | bundling: { 31 | commandHooks: { 32 | beforeBundling(inputDir: string, outputDir: string): string[] { 33 | return [ 34 | `cp ${inputDir}/src/lambda/msg.txt ${outputDir}`, 35 | ]; 36 | }, 37 | afterBundling(_inputDir: string, _outputDir: string): string[] { 38 | return []; 39 | }, 40 | beforeInstall() { 41 | return []; 42 | }, 43 | }, 44 | }, 45 | architecture: Architecture.ARM_64, 46 | timeout: Duration.seconds(5), 47 | memorySize: 128, 48 | runtime: Runtime.NODEJS_14_X, 49 | }); 50 | 51 | const echoIntegration = new HttpLambdaIntegration('EchoIntegration', echoFunc, { 52 | payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, 53 | }); 54 | const httpApi = new HttpApi(this, 'HttpApi'); 55 | httpApi.addRoutes({ 56 | path: '/', 57 | methods: [HttpMethod.GET], 58 | integration: echoIntegration, 59 | }); 60 | 61 | new CfnOutput(this, 'ApiUrl', { 62 | value: httpApi.apiEndpoint, 63 | description: 'url of api', 64 | }); 65 | } 66 | } 67 | 68 | const app = new App(); 69 | 70 | new AppStack(app, 'AppStack', { 71 | synthesizer: synthesizer(), 72 | }); 73 | 74 | if (process.env.USE_BSS) { 75 | Aspects.of(app).add(new CompositeECRRepositoryAspect()); 76 | } 77 | 78 | function synthesizer() { 79 | return process.env.USE_BSS ? new BootstraplessStackSynthesizer(): undefined; 80 | } 81 | -------------------------------------------------------------------------------- /sample-pipeline/src/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/nginx/nginx:1.21-alpine 2 | COPY index.html /usr/share/nginx/html -------------------------------------------------------------------------------- /sample-pipeline/src/app/index.html: -------------------------------------------------------------------------------- 1 |
 2 | 	    __ __                     __  __     _ 
 3 | 	   / //_/_  ______  ____ _   / / / /__  (_)
 4 | 	  / ,< / / / / __ \/ __ `/  / /_/ / _ \/ / 
 5 | 	 / /| / /_/ / / / / /_/ /  / __  /  __/ /  
 6 | 	/_/ |_\__,_/_/ /_/\__, /  /_/ /_/\___/_/   
 7 | 	                 /____/                    
 8 | 	    ______      __     ________               
 9 | 	   / ____/___ _/ /_   / ____/ /_  ____  __  __
10 | 	  / /_  / __ `/ __/  / /   / __ \/ __ \/ / / /
11 | 	 / __/ / /_/ / /_   / /___/ / / / /_/ / /_/ / 
12 | 	/_/    \__,_/\__/   \____/_/ /_/\____/\__, /  
13 |         	                             /____/   
14 | 
15 |                                                                  
16 |                        ___......----:'"":--....(\
17 |                 .-':'"":   :  :  :   :  :  :.(1\.`-.  
18 |               .'`.  `.  :  :  :   :   : : : : : :  .';
19 |              :-`. :   .  : :  `.  :   : :.   : :`.`. a;
20 |              : ;-. `-.-._.  :  :   :  ::. .' `. `., =  ;
21 |              :-:.` .-. _-.,  :  :  : ::,.'.-' ;-. ,'''"
22 |            .'.' ;`. .-' `-.:  :  : : :;.-'.-.'   `-'
23 |     :.   .'.'.-' .'`-.' -._;..:---'''"~;._.-;
24 |     :`--'.'  : :'     ;`-.;            :.`.-'`. 
25 |      `'"`    : :      ;`.;             :=; `.-'`.
26 |              : '.    :  ;              :-:   `._-`.
27 |               `'"'    `. `.            `--'     `._;
28 |                         `'"'    
29 |            | 
30 |      _____ | _____        \     /   /______    |  |     / /_  /    | |
31 |     ()____)+()____)     -----  /       |       | -+-.  /_.|_|/_.   | |
32 |     ()____)+()____)      \ /  /___   __|__   | |  | |   / | | /    | |
33 |     ()____)+()____)     ----- | |    | |     |_| _|_|_ /_\`-'/_\   | |
34 |     ()____)+()____)     __|__ | |  __|_|____   |  |     ___|___    | |
35 |     ()____)+()____)      /|\  | |      |       | / \     _/|\_     * *
36 |          / | \
37 | 
-------------------------------------------------------------------------------- /sample-pipeline/src/lambda/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { Logger } from '@aws-lambda-powertools/logger'; 3 | import { APIGatewayProxyHandler } from 'aws-lambda'; 4 | 5 | const logger = new Logger({ 6 | logLevel: 'INFO', 7 | }); 8 | 9 | const MSG = fs.readFileSync('./msg.txt', 'utf8'); 10 | 11 | export const handler: APIGatewayProxyHandler = async (para, _context)=> { 12 | logger.debug(`Receiving event ${JSON.stringify(para, null, 2)}.`); 13 | 14 | const result = { 15 | statusCode: 200, 16 | body: MSG, 17 | isBase64Encoded: false, 18 | }; 19 | logger.debug(`response result is ${JSON.stringify(result, null, 2)}`); 20 | 21 | return result; 22 | }; -------------------------------------------------------------------------------- /sample-pipeline/src/lambda/msg.txt: -------------------------------------------------------------------------------- 1 | __ __ __ __ _ 2 | / //_/_ ______ ____ _ / / / /__ (_) 3 | / ,< / / / / __ \/ __ `/ / /_/ / _ \/ / 4 | / /| / /_/ / / / / /_/ / / __ / __/ / 5 | /_/ |_\__,_/_/ /_/\__, / /_/ /_/\___/_/ 6 | /____/ 7 | ______ __ ________ 8 | / ____/___ _/ /_ / ____/ /_ ____ __ __ 9 | / /_ / __ `/ __/ / / / __ \/ __ \/ / / / 10 | / __/ / /_/ / /_ / /___/ / / / /_/ / /_/ / 11 | /_/ \__,_/\__/ \____/_/ /_/\____/\__, / 12 | /____/ 13 | 14 | 15 | ___......----:'"":--....(\ 16 | .-':'"": : : : : : :.(1\.`-. 17 | .'`. `. : : : : : : : : : : .'; 18 | :-`. : . : : `. : : :. : :`.`. a; 19 | : ;-. `-.-._. : : : ::. .' `. `., = ; 20 | :-:.` .-. _-., : : : ::,.'.-' ;-. ,'''" 21 | .'.' ;`. .-' `-.: : : : :;.-'.-.' `-' 22 | :. .'.'.-' .'`-.' -._;..:---'''"~;._.-; 23 | :`--'.' : :' ;`-.; :.`.-'`. 24 | `'"` : : ;`.; :=; `.-'`. 25 | : '. : ; :-: `._-`. 26 | `'"' `. `. `--' `._; 27 | `'"' 28 | | 29 | _____ | _____ \ / /______ | | / /_ / | | 30 | ()____)+()____) ----- / | | -+-. /_.|_|/_. | | 31 | ()____)+()____) \ / /___ __|__ | | | | / | | / | | 32 | ()____)+()____) ----- | | | | |_| _|_|_ /_\`-'/_\ | | 33 | ()____)+()____) __|__ | | __|_|____ | | ___|___ | | 34 | ()____)+()____) /|\ | | | | / \ _/|\_ * * 35 | / | \ 36 | -------------------------------------------------------------------------------- /sample-pipeline/src/pipeline.ts: -------------------------------------------------------------------------------- 1 | import { Stack, StackProps, App, RemovalPolicy, Arn, Aws, Annotations, SecretValue } from 'aws-cdk-lib'; 2 | import { PipelineProject, BuildSpec, LinuxBuildImage, ComputeType, BuildEnvironmentVariableType } from 'aws-cdk-lib/aws-codebuild'; 3 | import { Pipeline, Artifact } from 'aws-cdk-lib/aws-codepipeline'; 4 | import { CodeBuildAction, GitHubSourceAction } from 'aws-cdk-lib/aws-codepipeline-actions'; 5 | import { Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 6 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 7 | import { Construct } from 'constructs'; 8 | 9 | /** 10 | * Stack to hold the pipeline 11 | */ 12 | export class CDKToCloudFormationPipelineStack extends Stack { 13 | constructor(scope: Construct, id: string, props?: StackProps) { 14 | super(scope, id, props); 15 | 16 | var githubToken = this.node.tryGetContext('GithubToken'); 17 | if (!githubToken) { 18 | Annotations.of(this).addWarning(`GithubToken is not specified, 19 | use github-token as default, pls make sure the token is saved as plaintext. 20 | `); 21 | githubToken = 'github-token'; 22 | } 23 | 24 | const publishAccountId = this.node.tryGetContext('PublishAccountId') ?? Aws.ACCOUNT_ID; 25 | 26 | const assetBucket = new Bucket(this, 'AssetsBucket', { 27 | removalPolicy: RemovalPolicy.DESTROY, 28 | autoDeleteObjects: true, 29 | }); 30 | 31 | const appName = 'myapp'; 32 | const fileAssetsPrefix = `${appName}/`; 33 | const regionSet: string = this.node.tryGetContext('RegionSet') ?? 'ap-southeast-1,ap-northeast-1,us-east-1,us-west-2'; 34 | 35 | const assetOutputPath = 'assets-output/'; 36 | 37 | const sourceOutput = new Artifact(); 38 | const buildOutput = new Artifact(); 39 | const publishCodeBuildProject = new PipelineProject(this, 'PublishCloudFormation', { 40 | environment: { 41 | buildImage: LinuxBuildImage.STANDARD_5_0, 42 | computeType: ComputeType.SMALL, 43 | privileged: true, 44 | }, 45 | buildSpec: BuildSpec.fromObjectToYaml({ 46 | version: '0.2', 47 | env: { 48 | shell: 'bash', 49 | variables: { 50 | BSS_TEMPLATE_BUCKET_NAME: assetBucket.bucketName, 51 | BSS_IMAGE_ASSET_REPOSITORY_NAME: appName, 52 | FILE_ASSET_PREFIX: fileAssetsPrefix, 53 | REGIONS: regionSet, 54 | }, 55 | }, 56 | phases: { 57 | install: { 58 | 'on-failure': 'ABORT', 59 | 'runtime-versions': { 60 | nodejs: '14', 61 | }, 62 | 'commands': [ 63 | 'npm install -g cdk-assets', 64 | ], 65 | }, 66 | pre_build: { 67 | 'on-failure': 'ABORT', 68 | 'commands': [ 69 | 'export BSS_FILE_ASSET_PREFIX="${FILE_ASSET_PREFIX}${BUILD_VERSION}/"', 70 | ` 71 | #!/bin/bash 72 | set -euxo 73 | 74 | create_repo() { 75 | local name=$1 76 | local region=$2 77 | 78 | # create ecr repo 79 | aws ecr create-repository --region $region --repository-name "$name" --image-tag-mutability IMMUTABLE --image-scanning-configuration scanOnPush=true --encryption-configuration encryptionType=KMS 2>/dev/null 80 | 81 | set +e 82 | # set repo permission 83 | read -r -d '' POLICY_TEXT << EOM 84 | { 85 | "Version": "2008-10-17", 86 | "Statement": [ 87 | { 88 | "Sid": "public statement", 89 | "Effect": "Allow", 90 | "Principal": "*", 91 | "Action": [ 92 | "ecr:BatchCheckLayerAvailability", 93 | "ecr:BatchGetImage", 94 | "ecr:GetDownloadUrlForLayer" 95 | ] 96 | } 97 | ] 98 | } 99 | EOM 100 | set -e 101 | 102 | aws ecr set-repository-policy --region $region --repository-name "$name" --policy-text "$POLICY_TEXT" 2>/dev/null 103 | } 104 | 105 | create_s3_bucket() { 106 | local name=$1 107 | local region=$2 108 | 109 | EXIT_CODE=0 110 | aws s3 ls s3://$name --region $region || EXIT_CODE=$? 111 | if [[ $EXIT_CODE -eq 0 ]]; then 112 | echo "The bucket with name '$name' already exists." 113 | else 114 | aws s3 mb "s3://$name" --region $region 115 | echo "The bucket with name '$name' is created in region '$region'." 116 | fi 117 | } 118 | 119 | create_s3_bucket "$BSS_TEMPLATE_BUCKET_NAME" "us-east-1" 120 | 121 | for i in \${REGIONS//,/ } 122 | do 123 | echo "Prepase S3 resource in region '$i'" 124 | create_s3_bucket "$BSS_TEMPLATE_BUCKET_NAME-$i" "$i" 125 | done 126 | 127 | for i in \${REGIONS//,/ } 128 | do 129 | echo "Initial ECR repo in region '$i'" 130 | 131 | EXISTINGREPO=$(aws ecr describe-repositories --region $i --repository-names $BSS_IMAGE_ASSET_REPOSITORY_NAME --query 'repositories[].repositoryName' 2>/dev/null|jq '.[]'|jq '.') 132 | if [[ -z $EXISTINGREPO ]] 133 | then 134 | create_repo "$BSS_IMAGE_ASSET_REPOSITORY_NAME" "$i" 135 | echo "The repo with name '$BSS_IMAGE_ASSET_REPOSITORY_NAME' is created in region '$i'." 136 | else 137 | echo "The repo with name '$BSS_IMAGE_ASSET_REPOSITORY_NAME' already exists in region '$i'." 138 | fi 139 | done 140 | `, 141 | ], 142 | }, 143 | build: { 144 | 'on-failure': 'ABORT', 145 | 'commands': [ 146 | ` 147 | #!/bin/bash 148 | set -euxo pipefail 149 | 150 | cdk_assets_publish() { 151 | local assetsPath=$1 152 | echo "publish assets in '$assetsPath'" 153 | 154 | for path in \`ls "$assetsPath"*.assets.json\` 155 | do 156 | echo "publish assets defined in file '$path'" 157 | cdk-assets publish -p "$path" -v 158 | done 159 | } 160 | 161 | cdk_assets_publish ${assetOutputPath} 162 | `, 163 | ], 164 | }, 165 | post_build: { 166 | 'on-failure': 'ABORT', 167 | 'commands': [ 168 | ` 169 | #!/bin/bash 170 | set -euxo pipefail 171 | 172 | publish_s3_assets() { 173 | local name=$1 174 | local prefix=$2 175 | local region=$3 176 | 177 | KEY=\`aws s3api list-objects-v2 --bucket "$name" --prefix "$prefix" --max-item 1 --region $region | jq -r '.Contents[0].Key'\` 178 | if [ ! -z "$KEY" ]; then 179 | aws s3 ls s3://$name/$prefix --recursive --region $region | awk '{print $4}' | xargs -I {} -n 1 aws s3api put-object-acl --region $region --acl public-read --bucket $name --key {} 180 | fi 181 | } 182 | 183 | 184 | publish_s3_assets "$BSS_TEMPLATE_BUCKET_NAME" "$BSS_FILE_ASSET_PREFIX" us-east-1 185 | 186 | for i in \${REGIONS//,/ } 187 | do 188 | echo "Publish S3 resource in bucket '"$BSS_TEMPLATE_BUCKET_NAME-$i"'" 189 | publish_s3_assets "$BSS_TEMPLATE_BUCKET_NAME-$i" "$BSS_FILE_ASSET_PREFIX" "$i" 190 | done 191 | `, 192 | ` 193 | echo '======CloudFormation Url======' 194 | ls ${assetOutputPath}*.template.json | grep -v nested | sed 's/${assetOutputPath}/g' | cut -c 2- | xargs -I {} echo "https://${assetBucket.bucketName}.s3.${Aws.URL_SUFFIX}/\$BSS_FILE_ASSET_PREFIX{}" 195 | `, 196 | ], 197 | }, 198 | }, 199 | 200 | }), 201 | }); 202 | publishCodeBuildProject.role!.attachInlinePolicy(new Policy(this, 'PublishPolicy', { 203 | statements: [ 204 | new PolicyStatement({ 205 | sid: 'ecr1', 206 | actions: [ 207 | 'ecr:GetAuthorizationToken', 208 | ], 209 | resources: [ 210 | '*', 211 | ], 212 | }), 213 | new PolicyStatement({ 214 | sid: 'ecr2', 215 | actions: [ 216 | 'ecr:ListImages', 217 | 'ecr:BatchCheckLayerAvailability', 218 | 'ecr:GetDownloadUrlForLayer', 219 | 'ecr:GetRepositoryPolicy', 220 | 'ecr:DescribeRepositories', 221 | 'ecr:ListImages', 222 | 'ecr:DescribeImages', 223 | 'ecr:BatchGetImage', 224 | 'ecr:InitiateLayerUpload', 225 | 'ecr:UploadLayerPart', 226 | 'ecr:CompleteLayerUpload', 227 | 'ecr:PutImage', 228 | 'ecr:CreateRepository', 229 | 'ecr:SetRepositoryPolicy', 230 | ], 231 | resources: regionSet.split(',').map(r => Arn.format({ 232 | service: 'ecr', 233 | region: r, 234 | resource: 'repository', 235 | resourceName: appName, 236 | }, Stack.of(this))), 237 | }), 238 | new PolicyStatement({ 239 | sid: 's31', 240 | actions: [ 241 | 's3:ListBucket', 242 | 's3:CreateBucket', 243 | ], 244 | resources: [ 245 | ...regionSet.split(',').map(r => Arn.format({ 246 | service: 's3', 247 | region: '', 248 | account: '', 249 | resource: `${assetBucket.bucketName}-${r}`, 250 | }, Stack.of(this))), 251 | assetBucket.bucketArn, 252 | ], 253 | }), 254 | new PolicyStatement({ 255 | sid: 's32', 256 | actions: [ 257 | 's3:GetBucketAcl', 258 | 's3:GetBucketLocation', 259 | 's3:GetEncryptionConfiguration', 260 | ], 261 | resources: [ 262 | ...regionSet.split(',').map(r => Arn.format({ 263 | service: 's3', 264 | region: '', 265 | account: '', 266 | resource: `${assetBucket.bucketName}-${r}`, 267 | }, Stack.of(this))), 268 | assetBucket.bucketArn, 269 | ], 270 | }), 271 | new PolicyStatement({ 272 | sid: 's33', 273 | actions: [ 274 | 's3:PutObject', 275 | 's3:PutObjectAcl', 276 | ], 277 | resources: [ 278 | ...regionSet.split(',').map(r => Arn.format({ 279 | service: 's3', 280 | region: '', 281 | account: '', 282 | resource: `${assetBucket.bucketName}-${r}`, 283 | resourceName: `${fileAssetsPrefix}*`, 284 | }, Stack.of(this))), 285 | assetBucket.arnForObjects(`${fileAssetsPrefix}*`), 286 | ], 287 | }), 288 | ], 289 | })); 290 | new Pipeline(this, 'CDKToCloudFormationPublishPipeline', { 291 | stages: [ 292 | { 293 | stageName: 'Source', 294 | actions: [ 295 | new GitHubSourceAction({ 296 | actionName: 'GitHub_Source', 297 | owner: 'aws-samples', 298 | repo: 'cdk-bootstrapless-synthesizer', 299 | branch: 'main', 300 | oauthToken: SecretValue.secretsManager(githubToken), 301 | output: sourceOutput, 302 | }), 303 | ], 304 | }, 305 | { 306 | stageName: 'Build', 307 | actions: [ 308 | new CodeBuildAction({ 309 | actionName: 'CodeBuild', 310 | variablesNamespace: 'build', 311 | project: new PipelineProject(this, 'BuildApp', { 312 | environment: { 313 | buildImage: LinuxBuildImage.STANDARD_5_0, 314 | computeType: ComputeType.SMALL, 315 | privileged: true, 316 | }, 317 | buildSpec: BuildSpec.fromObjectToYaml({ 318 | version: '0.2', 319 | env: { 320 | 'shell': 'bash', 321 | 'exported-variables': [ 322 | 'BUILD_VERSION', 323 | ], 324 | 'variables': { 325 | USE_BSS: 'true', 326 | BSS_TEMPLATE_BUCKET_NAME: assetBucket.bucketName, 327 | BSS_FILE_ASSET_BUCKET_NAME: `${assetBucket.bucketName}-\${AWS::Region}`, 328 | BSS_FILE_ASSET_REGION_SET: regionSet, 329 | FILE_ASSET_PREFIX: fileAssetsPrefix, 330 | BSS_IMAGE_ASSET_REPOSITORY_NAME: appName, 331 | BSS_IMAGE_ASSET_ACCOUNT_ID: publishAccountId, 332 | BSS_IMAGE_ASSET_REGION_SET: regionSet, 333 | }, 334 | }, 335 | phases: { 336 | install: { 337 | 'runtime-versions': { 338 | nodejs: '14', 339 | }, 340 | 'commands': [ 341 | 'yarn install --check-files --frozen-lockfile', 342 | 'npx projen', 343 | ], 344 | }, 345 | pre_build: { 346 | commands: [ 347 | 'export BUILD_VERSION="v1.$(date +"%Y%m%d%H%M")"', 348 | 'export BSS_FILE_ASSET_PREFIX="${FILE_ASSET_PREFIX}${BUILD_VERSION}/"', 349 | ], 350 | }, 351 | build: { 352 | commands: [ 353 | `cd sample-pipeline; npx cdk synth AppStack --app "npx ts-node -P tsconfig.json --prefer-ts-exts src/app.ts" --json --output ${assetOutputPath} -q 2>/dev/null`, 354 | ], 355 | }, 356 | }, 357 | artifacts: { 358 | 'files': [ 359 | `${assetOutputPath}**/*`, 360 | ], 361 | 'base-directory': 'sample-pipeline', 362 | }, 363 | }), 364 | }), 365 | input: sourceOutput, 366 | outputs: [ 367 | buildOutput, 368 | ], 369 | }), 370 | ], 371 | }, 372 | { 373 | stageName: 'Publish', 374 | actions: [ 375 | new CodeBuildAction({ 376 | actionName: 'CodeBuild', 377 | project: publishCodeBuildProject, 378 | environmentVariables: { 379 | BUILD_VERSION: { 380 | type: BuildEnvironmentVariableType.PLAINTEXT, 381 | value: '#{build.BUILD_VERSION}', 382 | }, 383 | }, 384 | input: buildOutput, 385 | }), 386 | ], 387 | }, 388 | ], 389 | }); 390 | 391 | } 392 | } 393 | 394 | const app = new App(); 395 | new CDKToCloudFormationPipelineStack(app, 'PipelineStack', { 396 | env: { 397 | account: process.env.CDK_DEFAULT_ACCOUNT, 398 | region: process.env.CDK_DEFAULT_REGION, 399 | }, 400 | }); -------------------------------------------------------------------------------- /sample-pipeline/test/pipeline.test.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import { CDKToCloudFormationPipelineStack } from '../src/pipeline'; 4 | 5 | test('Snapshot', () => { 6 | const stack = new CDKToCloudFormationPipelineStack(new App(), 'test'); 7 | const template = Template.fromStack(stack); 8 | expect(template.toJSON()).toMatchSnapshot(); 9 | }); -------------------------------------------------------------------------------- /sample-pipeline/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "inlineSourceMap": true, 8 | "inlineSources": true, 9 | "lib": [ 10 | "es2019" 11 | ], 12 | "module": "CommonJS", 13 | "noEmitOnError": false, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "noImplicitThis": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "resolveJsonModule": true, 21 | "strict": true, 22 | "strictNullChecks": true, 23 | "strictPropertyInitialization": true, 24 | "stripInternal": true, 25 | "target": "ES2019" 26 | }, 27 | "include": [ 28 | ".projenrc.js", 29 | "src/**/*.ts", 30 | "test/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ], 35 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 36 | } 37 | -------------------------------------------------------------------------------- /sample-pipeline/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "lib", 5 | "alwaysStrict": true, 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "inlineSourceMap": true, 10 | "inlineSources": true, 11 | "lib": [ 12 | "es2019" 13 | ], 14 | "module": "CommonJS", 15 | "noEmitOnError": false, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitAny": true, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "strict": true, 24 | "strictNullChecks": true, 25 | "strictPropertyInitialization": true, 26 | "stripInternal": true, 27 | "target": "ES2019" 28 | }, 29 | "include": [ 30 | "src/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "cdk.out" 34 | ], 35 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 36 | } 37 | -------------------------------------------------------------------------------- /sample/.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js 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 | ], 21 | "settings": { 22 | "import/parsers": { 23 | "@typescript-eslint/parser": [ 24 | ".ts", 25 | ".tsx" 26 | ] 27 | }, 28 | "import/resolver": { 29 | "node": {}, 30 | "typescript": { 31 | "project": "./tsconfig.dev.json", 32 | "alwaysTryTypes": true 33 | } 34 | } 35 | }, 36 | "ignorePatterns": [ 37 | "*.js", 38 | "!.projenrc.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage" 43 | ], 44 | "rules": { 45 | "indent": [ 46 | "off" 47 | ], 48 | "@typescript-eslint/indent": [ 49 | "error", 50 | 2 51 | ], 52 | "quotes": [ 53 | "error", 54 | "single", 55 | { 56 | "avoidEscape": true 57 | } 58 | ], 59 | "comma-dangle": [ 60 | "error", 61 | "always-multiline" 62 | ], 63 | "comma-spacing": [ 64 | "error", 65 | { 66 | "before": false, 67 | "after": true 68 | } 69 | ], 70 | "no-multi-spaces": [ 71 | "error", 72 | { 73 | "ignoreEOLComments": false 74 | } 75 | ], 76 | "array-bracket-spacing": [ 77 | "error", 78 | "never" 79 | ], 80 | "array-bracket-newline": [ 81 | "error", 82 | "consistent" 83 | ], 84 | "object-curly-spacing": [ 85 | "error", 86 | "always" 87 | ], 88 | "object-curly-newline": [ 89 | "error", 90 | { 91 | "multiline": true, 92 | "consistent": true 93 | } 94 | ], 95 | "object-property-newline": [ 96 | "error", 97 | { 98 | "allowAllPropertiesOnSameLine": true 99 | } 100 | ], 101 | "keyword-spacing": [ 102 | "error" 103 | ], 104 | "brace-style": [ 105 | "error", 106 | "1tbs", 107 | { 108 | "allowSingleLine": true 109 | } 110 | ], 111 | "space-before-blocks": [ 112 | "error" 113 | ], 114 | "curly": [ 115 | "error", 116 | "multi-line", 117 | "consistent" 118 | ], 119 | "@typescript-eslint/member-delimiter-style": [ 120 | "error" 121 | ], 122 | "semi": [ 123 | "error", 124 | "always" 125 | ], 126 | "max-len": [ 127 | "error", 128 | { 129 | "code": 150, 130 | "ignoreUrls": true, 131 | "ignoreStrings": true, 132 | "ignoreTemplateLiterals": true, 133 | "ignoreComments": true, 134 | "ignoreRegExpLiterals": true 135 | } 136 | ], 137 | "quote-props": [ 138 | "error", 139 | "consistent-as-needed" 140 | ], 141 | "@typescript-eslint/no-require-imports": [ 142 | "error" 143 | ], 144 | "import/no-extraneous-dependencies": [ 145 | "error", 146 | { 147 | "devDependencies": [ 148 | "**/test/**", 149 | "**/build-tools/**" 150 | ], 151 | "optionalDependencies": false, 152 | "peerDependencies": true 153 | } 154 | ], 155 | "import/no-unresolved": [ 156 | "error" 157 | ], 158 | "import/order": [ 159 | "warn", 160 | { 161 | "groups": [ 162 | "builtin", 163 | "external" 164 | ], 165 | "alphabetize": { 166 | "order": "asc", 167 | "caseInsensitive": true 168 | } 169 | } 170 | ], 171 | "no-duplicate-imports": [ 172 | "error" 173 | ], 174 | "no-shadow": [ 175 | "off" 176 | ], 177 | "@typescript-eslint/no-shadow": [ 178 | "error" 179 | ], 180 | "key-spacing": [ 181 | "error" 182 | ], 183 | "no-multiple-empty-lines": [ 184 | "error" 185 | ], 186 | "@typescript-eslint/no-floating-promises": [ 187 | "error" 188 | ], 189 | "no-return-await": [ 190 | "off" 191 | ], 192 | "@typescript-eslint/return-await": [ 193 | "error" 194 | ], 195 | "no-trailing-spaces": [ 196 | "error" 197 | ], 198 | "dot-notation": [ 199 | "error" 200 | ], 201 | "no-bitwise": [ 202 | "error" 203 | ], 204 | "@typescript-eslint/member-ordering": [ 205 | "error", 206 | { 207 | "default": [ 208 | "public-static-field", 209 | "public-static-method", 210 | "protected-static-field", 211 | "protected-static-method", 212 | "private-static-field", 213 | "private-static-method", 214 | "field", 215 | "constructor", 216 | "method" 217 | ] 218 | } 219 | ] 220 | }, 221 | "overrides": [ 222 | { 223 | "files": [ 224 | ".projenrc.js" 225 | ], 226 | "rules": { 227 | "@typescript-eslint/no-require-imports": "off", 228 | "import/no-extraneous-dependencies": "off" 229 | } 230 | } 231 | ] 232 | } 233 | -------------------------------------------------------------------------------- /sample/.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | /.gitattributes linguist-generated 4 | /.projen/** linguist-generated 5 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/package.json 7 | !/.npmignore 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | lib-cov 20 | coverage 21 | *.lcov 22 | .nyc_output 23 | build/Release 24 | node_modules/ 25 | jspm_packages/ 26 | *.tsbuildinfo 27 | .eslintcache 28 | *.tgz 29 | .yarn-integrity 30 | .cache 31 | !/.projenrc.js 32 | /test-reports/ 33 | junit.xml 34 | /coverage/ 35 | !/test/ 36 | !/tsconfig.json 37 | !/tsconfig.dev.json 38 | !/src/ 39 | /lib 40 | /dist/ 41 | !/.eslintrc.json 42 | /assets/ 43 | !/cdk.json 44 | /cdk.out/ 45 | .cdk.staging/ 46 | .parcel-cache/ 47 | -------------------------------------------------------------------------------- /sample/.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | /test/ 7 | /tsconfig.dev.json 8 | /src/ 9 | !/lib/ 10 | !/lib/**/*.js 11 | !/lib/**/*.d.ts 12 | dist 13 | /tsconfig.json 14 | /.github/ 15 | /.vscode/ 16 | /.idea/ 17 | /.projenrc.js 18 | tsconfig.tsbuildinfo 19 | /.eslintrc.json 20 | !/assets/ 21 | cdk.out/ 22 | .cdk.staging/ 23 | -------------------------------------------------------------------------------- /sample/.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@types/jest", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@types/node", 9 | "version": "^14", 10 | "type": "build" 11 | }, 12 | { 13 | "name": "@typescript-eslint/eslint-plugin", 14 | "version": "^5", 15 | "type": "build" 16 | }, 17 | { 18 | "name": "@typescript-eslint/parser", 19 | "version": "^5", 20 | "type": "build" 21 | }, 22 | { 23 | "name": "ansi-regex", 24 | "version": "6.0.1", 25 | "type": "build" 26 | }, 27 | { 28 | "name": "aws-cdk", 29 | "version": "^2.3.0", 30 | "type": "build" 31 | }, 32 | { 33 | "name": "esbuild", 34 | "type": "build" 35 | }, 36 | { 37 | "name": "eslint-import-resolver-node", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "eslint-import-resolver-typescript", 42 | "type": "build" 43 | }, 44 | { 45 | "name": "eslint-plugin-import", 46 | "type": "build" 47 | }, 48 | { 49 | "name": "eslint", 50 | "version": "^8", 51 | "type": "build" 52 | }, 53 | { 54 | "name": "jest", 55 | "type": "build" 56 | }, 57 | { 58 | "name": "jest-junit", 59 | "version": "^13", 60 | "type": "build" 61 | }, 62 | { 63 | "name": "json-schema", 64 | "type": "build" 65 | }, 66 | { 67 | "name": "npm-check-updates", 68 | "version": "^16", 69 | "type": "build" 70 | }, 71 | { 72 | "name": "projen", 73 | "version": "^0.50.31", 74 | "type": "build" 75 | }, 76 | { 77 | "name": "ts-jest", 78 | "type": "build" 79 | }, 80 | { 81 | "name": "ts-node", 82 | "type": "build" 83 | }, 84 | { 85 | "name": "typescript", 86 | "type": "build" 87 | }, 88 | { 89 | "name": "aws-cdk-lib", 90 | "version": "^2.3.0", 91 | "type": "runtime" 92 | }, 93 | { 94 | "name": "cdk-bootstrapless-synthesizer", 95 | "version": "^2", 96 | "type": "runtime" 97 | }, 98 | { 99 | "name": "constructs", 100 | "version": "^10.0.5", 101 | "type": "runtime" 102 | } 103 | ], 104 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 105 | } 106 | -------------------------------------------------------------------------------- /sample/.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".gitignore", 6 | ".npmignore", 7 | ".projen/deps.json", 8 | ".projen/files.json", 9 | ".projen/tasks.json", 10 | "cdk.json", 11 | "tsconfig.dev.json", 12 | "tsconfig.json" 13 | ], 14 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 15 | } 16 | -------------------------------------------------------------------------------- /sample/.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "pre-compile" 9 | }, 10 | { 11 | "spawn": "compile" 12 | }, 13 | { 14 | "spawn": "post-compile" 15 | }, 16 | { 17 | "spawn": "test" 18 | }, 19 | { 20 | "spawn": "package" 21 | } 22 | ] 23 | }, 24 | "bundle": { 25 | "name": "bundle", 26 | "description": "Prepare assets" 27 | }, 28 | "clobber": { 29 | "name": "clobber", 30 | "description": "hard resets to HEAD of origin and cleans the local repo", 31 | "env": { 32 | "BRANCH": "$(git branch --show-current)" 33 | }, 34 | "steps": [ 35 | { 36 | "exec": "git checkout -b scratch", 37 | "name": "save current HEAD in \"scratch\" branch" 38 | }, 39 | { 40 | "exec": "git checkout $BRANCH" 41 | }, 42 | { 43 | "exec": "git fetch origin", 44 | "name": "fetch latest changes from origin" 45 | }, 46 | { 47 | "exec": "git reset --hard origin/$BRANCH", 48 | "name": "hard reset to origin commit" 49 | }, 50 | { 51 | "exec": "git clean -fdx", 52 | "name": "clean all untracked files" 53 | }, 54 | { 55 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 56 | } 57 | ], 58 | "condition": "git diff --exit-code > /dev/null" 59 | }, 60 | "compile": { 61 | "name": "compile", 62 | "description": "Only compile" 63 | }, 64 | "default": { 65 | "name": "default", 66 | "description": "Synthesize project files", 67 | "steps": [ 68 | { 69 | "exec": "node .projenrc.js" 70 | } 71 | ] 72 | }, 73 | "deploy": { 74 | "name": "deploy", 75 | "description": "Deploys your CDK app to the AWS cloud", 76 | "steps": [ 77 | { 78 | "exec": "cdk deploy", 79 | "receiveArgs": true 80 | } 81 | ] 82 | }, 83 | "destroy": { 84 | "name": "destroy", 85 | "description": "Destroys your cdk app in the AWS cloud", 86 | "steps": [ 87 | { 88 | "exec": "cdk destroy", 89 | "receiveArgs": true 90 | } 91 | ] 92 | }, 93 | "diff": { 94 | "name": "diff", 95 | "description": "Diffs the currently deployed app against your code", 96 | "steps": [ 97 | { 98 | "exec": "cdk diff" 99 | } 100 | ] 101 | }, 102 | "eject": { 103 | "name": "eject", 104 | "description": "Remove projen from the project", 105 | "env": { 106 | "PROJEN_EJECTING": "true" 107 | }, 108 | "steps": [ 109 | { 110 | "spawn": "default" 111 | } 112 | ] 113 | }, 114 | "eslint": { 115 | "name": "eslint", 116 | "description": "Runs eslint against the codebase", 117 | "steps": [ 118 | { 119 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test build-tools .projenrc.js" 120 | } 121 | ] 122 | }, 123 | "package": { 124 | "name": "package", 125 | "description": "Creates the distribution package" 126 | }, 127 | "post-compile": { 128 | "name": "post-compile", 129 | "description": "Runs after successful compilation", 130 | "steps": [ 131 | { 132 | "spawn": "synth:silent" 133 | } 134 | ] 135 | }, 136 | "post-upgrade": { 137 | "name": "post-upgrade", 138 | "description": "Runs after upgrading dependencies" 139 | }, 140 | "pre-compile": { 141 | "name": "pre-compile", 142 | "description": "Prepare the project for compilation" 143 | }, 144 | "synth": { 145 | "name": "synth", 146 | "description": "Synthesizes your cdk app into cdk.out", 147 | "steps": [ 148 | { 149 | "exec": "cdk synth" 150 | } 151 | ] 152 | }, 153 | "synth:silent": { 154 | "name": "synth:silent", 155 | "description": "Synthesizes your cdk app into cdk.out and suppresses the template in stdout (part of \"yarn build\")", 156 | "steps": [ 157 | { 158 | "exec": "cdk synth -q" 159 | } 160 | ] 161 | }, 162 | "test": { 163 | "name": "test", 164 | "description": "Run tests", 165 | "steps": [ 166 | { 167 | "exec": "jest --passWithNoTests --updateSnapshot", 168 | "receiveArgs": true 169 | }, 170 | { 171 | "spawn": "eslint" 172 | } 173 | ] 174 | }, 175 | "test:watch": { 176 | "name": "test:watch", 177 | "description": "Run jest in watch mode", 178 | "steps": [ 179 | { 180 | "exec": "jest --watch" 181 | } 182 | ] 183 | }, 184 | "upgrade": { 185 | "name": "upgrade", 186 | "description": "upgrade dependencies", 187 | "env": { 188 | "CI": "0" 189 | }, 190 | "steps": [ 191 | { 192 | "exec": "yarn upgrade npm-check-updates" 193 | }, 194 | { 195 | "exec": "npm-check-updates --dep dev --upgrade --target=minor --reject='ansi-regex'" 196 | }, 197 | { 198 | "exec": "npm-check-updates --dep optional --upgrade --target=minor --reject='ansi-regex'" 199 | }, 200 | { 201 | "exec": "npm-check-updates --dep peer --upgrade --target=minor --reject='ansi-regex'" 202 | }, 203 | { 204 | "exec": "npm-check-updates --dep prod --upgrade --target=minor --reject='ansi-regex'" 205 | }, 206 | { 207 | "exec": "npm-check-updates --dep bundle --upgrade --target=minor --reject='ansi-regex'" 208 | }, 209 | { 210 | "exec": "yarn install --check-files" 211 | }, 212 | { 213 | "exec": "yarn upgrade" 214 | }, 215 | { 216 | "exec": "npx projen" 217 | }, 218 | { 219 | "spawn": "post-upgrade" 220 | } 221 | ] 222 | }, 223 | "watch": { 224 | "name": "watch", 225 | "description": "Watches changes in your source code and rebuilds and deploys to the current account", 226 | "steps": [ 227 | { 228 | "exec": "cdk deploy --hotswap" 229 | }, 230 | { 231 | "exec": "cdk watch" 232 | } 233 | ] 234 | } 235 | }, 236 | "env": { 237 | "PATH": "$(npx -c \"node -e \\\"console.log(process.env.PATH)\\\"\")" 238 | }, 239 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 240 | } 241 | -------------------------------------------------------------------------------- /sample/README.md: -------------------------------------------------------------------------------- 1 | ```shell 2 | $ yarn 3 | $ yarn synth 4 | $ npx cdk-assets publish -p cdk.out/my-stack-dev.assets.json -v 5 | ``` -------------------------------------------------------------------------------- /sample/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node -P tsconfig.json --prefer-ts-exts src/main.ts", 3 | "output": "cdk.out", 4 | "build": "npx projen bundle", 5 | "watch": { 6 | "include": [ 7 | "src/**/*.ts", 8 | "test/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "README.md", 12 | "cdk*.json", 13 | "**/*.d.ts", 14 | "**/*.js", 15 | "tsconfig.json", 16 | "package*.json", 17 | "yarn.lock", 18 | "node_modules" 19 | ] 20 | }, 21 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 22 | } 23 | -------------------------------------------------------------------------------- /sample/cdk.out/my-stack-dev.assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "15.0.0", 3 | "files": { 4 | "my-stack-dev.template.json": { 5 | "source": { 6 | "path": "my-stack-dev.template.json", 7 | "packaging": "file" 8 | }, 9 | "destinations": { 10 | "current_account-current_region": { 11 | "bucketName": "cfn-template-bucket", 12 | "objectKey": "file-asset-prefix/latest/my-stack-dev.template.json", 13 | "region": "us-west-1" 14 | } 15 | } 16 | } 17 | }, 18 | "dockerImages": { 19 | "d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3": { 20 | "source": { 21 | "directory": "asset.d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3" 22 | }, 23 | "destinations": { 24 | "us-west-1": { 25 | "repositoryName": "your-ecr-repo-name", 26 | "imageTag": "latest-d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3", 27 | "region": "us-west-1" 28 | }, 29 | "us-west-2": { 30 | "repositoryName": "your-ecr-repo-name", 31 | "imageTag": "latest-d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3", 32 | "region": "us-west-2" 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /sample/cdk.out/my-stack-dev.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Outputs": { 3 | "output": { 4 | "Value": { 5 | "Fn::Sub": "1234567890.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/your-ecr-repo-name:latest-d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3" 6 | } 7 | } 8 | }, 9 | "Resources": { 10 | "TaskDefTaskRole1EDB4A67": { 11 | "Type": "AWS::IAM::Role", 12 | "Properties": { 13 | "AssumeRolePolicyDocument": { 14 | "Statement": [ 15 | { 16 | "Action": "sts:AssumeRole", 17 | "Effect": "Allow", 18 | "Principal": { 19 | "Service": "ecs-tasks.amazonaws.com" 20 | } 21 | } 22 | ], 23 | "Version": "2012-10-17" 24 | } 25 | }, 26 | "Metadata": { 27 | "aws:cdk:path": "my-stack-dev/TaskDef/TaskRole/Resource" 28 | } 29 | }, 30 | "TaskDef54694570": { 31 | "Type": "AWS::ECS::TaskDefinition", 32 | "Properties": { 33 | "ContainerDefinitions": [ 34 | { 35 | "Essential": true, 36 | "Image": { 37 | "Fn::Sub": "1234567890.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/your-ecr-repo-name:latest-d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3" 38 | }, 39 | "Memory": 512, 40 | "Name": "DefaultContainer" 41 | } 42 | ], 43 | "Cpu": "256", 44 | "ExecutionRoleArn": { 45 | "Fn::GetAtt": [ 46 | "TaskDefExecutionRoleB4775C97", 47 | "Arn" 48 | ] 49 | }, 50 | "Family": "mystackdevTaskDef141959DC", 51 | "Memory": "512", 52 | "NetworkMode": "awsvpc", 53 | "RequiresCompatibilities": [ 54 | "FARGATE" 55 | ], 56 | "TaskRoleArn": { 57 | "Fn::GetAtt": [ 58 | "TaskDefTaskRole1EDB4A67", 59 | "Arn" 60 | ] 61 | } 62 | }, 63 | "Metadata": { 64 | "aws:cdk:path": "my-stack-dev/TaskDef/Resource" 65 | } 66 | }, 67 | "TaskDefExecutionRoleB4775C97": { 68 | "Type": "AWS::IAM::Role", 69 | "Properties": { 70 | "AssumeRolePolicyDocument": { 71 | "Statement": [ 72 | { 73 | "Action": "sts:AssumeRole", 74 | "Effect": "Allow", 75 | "Principal": { 76 | "Service": "ecs-tasks.amazonaws.com" 77 | } 78 | } 79 | ], 80 | "Version": "2012-10-17" 81 | } 82 | }, 83 | "Metadata": { 84 | "aws:cdk:path": "my-stack-dev/TaskDef/ExecutionRole/Resource" 85 | } 86 | }, 87 | "TaskDefExecutionRoleDefaultPolicy0DBB737A": { 88 | "Type": "AWS::IAM::Policy", 89 | "Properties": { 90 | "PolicyDocument": { 91 | "Statement": [ 92 | { 93 | "Action": [ 94 | "ecr:BatchCheckLayerAvailability", 95 | "ecr:GetDownloadUrlForLayer", 96 | "ecr:BatchGetImage" 97 | ], 98 | "Effect": "Allow", 99 | "Resource": { 100 | "Fn::Join": [ 101 | "", 102 | [ 103 | "arn:", 104 | { 105 | "Ref": "AWS::Partition" 106 | }, 107 | ":ecr:", 108 | { 109 | "Ref": "AWS::Region" 110 | }, 111 | ":", 112 | { 113 | "Ref": "AWS::AccountId" 114 | }, 115 | ":repository/your-ecr-repo-name" 116 | ] 117 | ] 118 | } 119 | }, 120 | { 121 | "Action": "ecr:GetAuthorizationToken", 122 | "Effect": "Allow", 123 | "Resource": "*" 124 | } 125 | ], 126 | "Version": "2012-10-17" 127 | }, 128 | "PolicyName": "TaskDefExecutionRoleDefaultPolicy0DBB737A", 129 | "Roles": [ 130 | { 131 | "Ref": "TaskDefExecutionRoleB4775C97" 132 | } 133 | ] 134 | }, 135 | "Metadata": { 136 | "aws:cdk:path": "my-stack-dev/TaskDef/ExecutionRole/DefaultPolicy/Resource" 137 | } 138 | }, 139 | "CDKMetadata": { 140 | "Type": "AWS::CDK::Metadata", 141 | "Properties": { 142 | "Analytics": "v2:deflate64:H4sIAAAAAAAA/12PT4vCQAzFP4v3afwLnldF8KTUvUt2GktsO5FJ6iKl332ndUHwlOT3eC/JApYwm+CvZr6ospp/oDsb+soldOnIxwuqkinsxFcUDw2W9DUQlzTI6S7KJvG5QaWEFLo9xhKNvlGrHV05sLEEt72GTyLBkAPFN+sdYwNdLjUNhrGepGb/HMZX1/du3J+uLDmUg3Bs7d7a6CCVNvrRnfKL/9QgBcFNp4/5Cubr9O9NmbPYBuOGIH/VPwROJYsKAQAA" 143 | }, 144 | "Metadata": { 145 | "aws:cdk:path": "my-stack-dev/CDKMetadata/Default" 146 | }, 147 | "Condition": "CDKMetadataAvailable" 148 | } 149 | }, 150 | "Conditions": { 151 | "CDKMetadataAvailable": { 152 | "Fn::Or": [ 153 | { 154 | "Fn::Or": [ 155 | { 156 | "Fn::Equals": [ 157 | { 158 | "Ref": "AWS::Region" 159 | }, 160 | "af-south-1" 161 | ] 162 | }, 163 | { 164 | "Fn::Equals": [ 165 | { 166 | "Ref": "AWS::Region" 167 | }, 168 | "ap-east-1" 169 | ] 170 | }, 171 | { 172 | "Fn::Equals": [ 173 | { 174 | "Ref": "AWS::Region" 175 | }, 176 | "ap-northeast-1" 177 | ] 178 | }, 179 | { 180 | "Fn::Equals": [ 181 | { 182 | "Ref": "AWS::Region" 183 | }, 184 | "ap-northeast-2" 185 | ] 186 | }, 187 | { 188 | "Fn::Equals": [ 189 | { 190 | "Ref": "AWS::Region" 191 | }, 192 | "ap-south-1" 193 | ] 194 | }, 195 | { 196 | "Fn::Equals": [ 197 | { 198 | "Ref": "AWS::Region" 199 | }, 200 | "ap-southeast-1" 201 | ] 202 | }, 203 | { 204 | "Fn::Equals": [ 205 | { 206 | "Ref": "AWS::Region" 207 | }, 208 | "ap-southeast-2" 209 | ] 210 | }, 211 | { 212 | "Fn::Equals": [ 213 | { 214 | "Ref": "AWS::Region" 215 | }, 216 | "ca-central-1" 217 | ] 218 | }, 219 | { 220 | "Fn::Equals": [ 221 | { 222 | "Ref": "AWS::Region" 223 | }, 224 | "cn-north-1" 225 | ] 226 | }, 227 | { 228 | "Fn::Equals": [ 229 | { 230 | "Ref": "AWS::Region" 231 | }, 232 | "cn-northwest-1" 233 | ] 234 | } 235 | ] 236 | }, 237 | { 238 | "Fn::Or": [ 239 | { 240 | "Fn::Equals": [ 241 | { 242 | "Ref": "AWS::Region" 243 | }, 244 | "eu-central-1" 245 | ] 246 | }, 247 | { 248 | "Fn::Equals": [ 249 | { 250 | "Ref": "AWS::Region" 251 | }, 252 | "eu-north-1" 253 | ] 254 | }, 255 | { 256 | "Fn::Equals": [ 257 | { 258 | "Ref": "AWS::Region" 259 | }, 260 | "eu-south-1" 261 | ] 262 | }, 263 | { 264 | "Fn::Equals": [ 265 | { 266 | "Ref": "AWS::Region" 267 | }, 268 | "eu-west-1" 269 | ] 270 | }, 271 | { 272 | "Fn::Equals": [ 273 | { 274 | "Ref": "AWS::Region" 275 | }, 276 | "eu-west-2" 277 | ] 278 | }, 279 | { 280 | "Fn::Equals": [ 281 | { 282 | "Ref": "AWS::Region" 283 | }, 284 | "eu-west-3" 285 | ] 286 | }, 287 | { 288 | "Fn::Equals": [ 289 | { 290 | "Ref": "AWS::Region" 291 | }, 292 | "me-south-1" 293 | ] 294 | }, 295 | { 296 | "Fn::Equals": [ 297 | { 298 | "Ref": "AWS::Region" 299 | }, 300 | "sa-east-1" 301 | ] 302 | }, 303 | { 304 | "Fn::Equals": [ 305 | { 306 | "Ref": "AWS::Region" 307 | }, 308 | "us-east-1" 309 | ] 310 | }, 311 | { 312 | "Fn::Equals": [ 313 | { 314 | "Ref": "AWS::Region" 315 | }, 316 | "us-east-2" 317 | ] 318 | } 319 | ] 320 | }, 321 | { 322 | "Fn::Or": [ 323 | { 324 | "Fn::Equals": [ 325 | { 326 | "Ref": "AWS::Region" 327 | }, 328 | "us-west-1" 329 | ] 330 | }, 331 | { 332 | "Fn::Equals": [ 333 | { 334 | "Ref": "AWS::Region" 335 | }, 336 | "us-west-2" 337 | ] 338 | } 339 | ] 340 | } 341 | ] 342 | } 343 | } 344 | } -------------------------------------------------------------------------------- /sample/cdk.out/my-stack-dev2.assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "15.0.0", 3 | "files": { 4 | "my-stack-dev2.template.json": { 5 | "source": { 6 | "path": "my-stack-dev2.template.json", 7 | "packaging": "file" 8 | }, 9 | "destinations": { 10 | "current_account-current_region": { 11 | "bucketName": "cfn-template-bucket", 12 | "objectKey": "file-asset-prefix/latest/my-stack-dev2.template.json", 13 | "region": "us-west-1" 14 | } 15 | } 16 | } 17 | }, 18 | "dockerImages": { 19 | "d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3": { 20 | "source": { 21 | "directory": "asset.d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3" 22 | }, 23 | "destinations": { 24 | "us-west-1": { 25 | "repositoryName": "your-ecr-repo-name", 26 | "imageTag": "latest-d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3", 27 | "region": "us-west-1" 28 | }, 29 | "us-west-2": { 30 | "repositoryName": "your-ecr-repo-name", 31 | "imageTag": "latest-d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3", 32 | "region": "us-west-2" 33 | } 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /sample/cdk.out/my-stack-dev2.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Outputs": { 3 | "output": { 4 | "Value": { 5 | "Fn::Sub": "1234567890.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/your-ecr-repo-name:latest-d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3" 6 | } 7 | } 8 | }, 9 | "Resources": { 10 | "TaskDefTaskRole1EDB4A67": { 11 | "Type": "AWS::IAM::Role", 12 | "Properties": { 13 | "AssumeRolePolicyDocument": { 14 | "Statement": [ 15 | { 16 | "Action": "sts:AssumeRole", 17 | "Effect": "Allow", 18 | "Principal": { 19 | "Service": "ecs-tasks.amazonaws.com" 20 | } 21 | } 22 | ], 23 | "Version": "2012-10-17" 24 | } 25 | }, 26 | "Metadata": { 27 | "aws:cdk:path": "my-stack-dev2/TaskDef/TaskRole/Resource" 28 | } 29 | }, 30 | "TaskDef54694570": { 31 | "Type": "AWS::ECS::TaskDefinition", 32 | "Properties": { 33 | "ContainerDefinitions": [ 34 | { 35 | "Essential": true, 36 | "Image": { 37 | "Fn::Sub": "1234567890.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/your-ecr-repo-name:latest-d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3" 38 | }, 39 | "Memory": 512, 40 | "Name": "DefaultContainer" 41 | } 42 | ], 43 | "Cpu": "256", 44 | "ExecutionRoleArn": { 45 | "Fn::GetAtt": [ 46 | "TaskDefExecutionRoleB4775C97", 47 | "Arn" 48 | ] 49 | }, 50 | "Family": "mystackdev2TaskDefF5BFA950", 51 | "Memory": "512", 52 | "NetworkMode": "awsvpc", 53 | "RequiresCompatibilities": [ 54 | "FARGATE" 55 | ], 56 | "TaskRoleArn": { 57 | "Fn::GetAtt": [ 58 | "TaskDefTaskRole1EDB4A67", 59 | "Arn" 60 | ] 61 | } 62 | }, 63 | "Metadata": { 64 | "aws:cdk:path": "my-stack-dev2/TaskDef/Resource" 65 | } 66 | }, 67 | "TaskDefExecutionRoleB4775C97": { 68 | "Type": "AWS::IAM::Role", 69 | "Properties": { 70 | "AssumeRolePolicyDocument": { 71 | "Statement": [ 72 | { 73 | "Action": "sts:AssumeRole", 74 | "Effect": "Allow", 75 | "Principal": { 76 | "Service": "ecs-tasks.amazonaws.com" 77 | } 78 | } 79 | ], 80 | "Version": "2012-10-17" 81 | } 82 | }, 83 | "Metadata": { 84 | "aws:cdk:path": "my-stack-dev2/TaskDef/ExecutionRole/Resource" 85 | } 86 | }, 87 | "TaskDefExecutionRoleDefaultPolicy0DBB737A": { 88 | "Type": "AWS::IAM::Policy", 89 | "Properties": { 90 | "PolicyDocument": { 91 | "Statement": [ 92 | { 93 | "Action": [ 94 | "ecr:BatchCheckLayerAvailability", 95 | "ecr:GetDownloadUrlForLayer", 96 | "ecr:BatchGetImage" 97 | ], 98 | "Effect": "Allow", 99 | "Resource": { 100 | "Fn::Join": [ 101 | "", 102 | [ 103 | "arn:", 104 | { 105 | "Ref": "AWS::Partition" 106 | }, 107 | ":ecr:", 108 | { 109 | "Ref": "AWS::Region" 110 | }, 111 | ":1234567890:repository/your-ecr-repo-name" 112 | ] 113 | ] 114 | } 115 | }, 116 | { 117 | "Action": "ecr:GetAuthorizationToken", 118 | "Effect": "Allow", 119 | "Resource": "*" 120 | } 121 | ], 122 | "Version": "2012-10-17" 123 | }, 124 | "PolicyName": "TaskDefExecutionRoleDefaultPolicy0DBB737A", 125 | "Roles": [ 126 | { 127 | "Ref": "TaskDefExecutionRoleB4775C97" 128 | } 129 | ] 130 | }, 131 | "Metadata": { 132 | "aws:cdk:path": "my-stack-dev2/TaskDef/ExecutionRole/DefaultPolicy/Resource" 133 | } 134 | }, 135 | "CDKMetadata": { 136 | "Type": "AWS::CDK::Metadata", 137 | "Properties": { 138 | "Analytics": "v2:deflate64:H4sIAAAAAAAA/12PT4vCQAzFP4v3afwLnldF8KTUvUt2GktsO5FJ6iKl332ndUHwlOT3eC/JApYwm+CvZr6ospp/oDsb+soldOnIxwuqkinsxFcUDw2W9DUQlzTI6S7KJvG5QaWEFLo9xhKNvlGrHV05sLEEt72GTyLBkAPFN+sdYwNdLjUNhrGepGb/HMZX1/du3J+uLDmUg3Bs7d7a6CCVNvrRnfKL/9QgBcFNp4/5Cubr9O9NmbPYBuOGIH/VPwROJYsKAQAA" 139 | }, 140 | "Metadata": { 141 | "aws:cdk:path": "my-stack-dev2/CDKMetadata/Default" 142 | }, 143 | "Condition": "CDKMetadataAvailable" 144 | } 145 | }, 146 | "Conditions": { 147 | "CDKMetadataAvailable": { 148 | "Fn::Or": [ 149 | { 150 | "Fn::Or": [ 151 | { 152 | "Fn::Equals": [ 153 | { 154 | "Ref": "AWS::Region" 155 | }, 156 | "af-south-1" 157 | ] 158 | }, 159 | { 160 | "Fn::Equals": [ 161 | { 162 | "Ref": "AWS::Region" 163 | }, 164 | "ap-east-1" 165 | ] 166 | }, 167 | { 168 | "Fn::Equals": [ 169 | { 170 | "Ref": "AWS::Region" 171 | }, 172 | "ap-northeast-1" 173 | ] 174 | }, 175 | { 176 | "Fn::Equals": [ 177 | { 178 | "Ref": "AWS::Region" 179 | }, 180 | "ap-northeast-2" 181 | ] 182 | }, 183 | { 184 | "Fn::Equals": [ 185 | { 186 | "Ref": "AWS::Region" 187 | }, 188 | "ap-south-1" 189 | ] 190 | }, 191 | { 192 | "Fn::Equals": [ 193 | { 194 | "Ref": "AWS::Region" 195 | }, 196 | "ap-southeast-1" 197 | ] 198 | }, 199 | { 200 | "Fn::Equals": [ 201 | { 202 | "Ref": "AWS::Region" 203 | }, 204 | "ap-southeast-2" 205 | ] 206 | }, 207 | { 208 | "Fn::Equals": [ 209 | { 210 | "Ref": "AWS::Region" 211 | }, 212 | "ca-central-1" 213 | ] 214 | }, 215 | { 216 | "Fn::Equals": [ 217 | { 218 | "Ref": "AWS::Region" 219 | }, 220 | "cn-north-1" 221 | ] 222 | }, 223 | { 224 | "Fn::Equals": [ 225 | { 226 | "Ref": "AWS::Region" 227 | }, 228 | "cn-northwest-1" 229 | ] 230 | } 231 | ] 232 | }, 233 | { 234 | "Fn::Or": [ 235 | { 236 | "Fn::Equals": [ 237 | { 238 | "Ref": "AWS::Region" 239 | }, 240 | "eu-central-1" 241 | ] 242 | }, 243 | { 244 | "Fn::Equals": [ 245 | { 246 | "Ref": "AWS::Region" 247 | }, 248 | "eu-north-1" 249 | ] 250 | }, 251 | { 252 | "Fn::Equals": [ 253 | { 254 | "Ref": "AWS::Region" 255 | }, 256 | "eu-south-1" 257 | ] 258 | }, 259 | { 260 | "Fn::Equals": [ 261 | { 262 | "Ref": "AWS::Region" 263 | }, 264 | "eu-west-1" 265 | ] 266 | }, 267 | { 268 | "Fn::Equals": [ 269 | { 270 | "Ref": "AWS::Region" 271 | }, 272 | "eu-west-2" 273 | ] 274 | }, 275 | { 276 | "Fn::Equals": [ 277 | { 278 | "Ref": "AWS::Region" 279 | }, 280 | "eu-west-3" 281 | ] 282 | }, 283 | { 284 | "Fn::Equals": [ 285 | { 286 | "Ref": "AWS::Region" 287 | }, 288 | "me-south-1" 289 | ] 290 | }, 291 | { 292 | "Fn::Equals": [ 293 | { 294 | "Ref": "AWS::Region" 295 | }, 296 | "sa-east-1" 297 | ] 298 | }, 299 | { 300 | "Fn::Equals": [ 301 | { 302 | "Ref": "AWS::Region" 303 | }, 304 | "us-east-1" 305 | ] 306 | }, 307 | { 308 | "Fn::Equals": [ 309 | { 310 | "Ref": "AWS::Region" 311 | }, 312 | "us-east-2" 313 | ] 314 | } 315 | ] 316 | }, 317 | { 318 | "Fn::Or": [ 319 | { 320 | "Fn::Equals": [ 321 | { 322 | "Ref": "AWS::Region" 323 | }, 324 | "us-west-1" 325 | ] 326 | }, 327 | { 328 | "Fn::Equals": [ 329 | { 330 | "Ref": "AWS::Region" 331 | }, 332 | "us-west-2" 333 | ] 334 | } 335 | ] 336 | } 337 | ] 338 | } 339 | } 340 | } -------------------------------------------------------------------------------- /sample/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx -------------------------------------------------------------------------------- /sample/lambda/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "uuid": { 8 | "version": "8.3.2", 9 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 10 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sample/lambda/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "src", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "uuid": "^8.3.2" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /sample/lambda/src/index.js: -------------------------------------------------------------------------------- 1 | const { v4: uuidv4 } = require('uuid'); 2 | 3 | exports.handler = async function(event) { 4 | console.log('request:', JSON.stringify(event, undefined, 2)); 5 | return { 6 | statusCode: 200, 7 | headers: { 'Content-Type': 'text/plain' }, 8 | body: uuidv4() 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /sample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sample", 3 | "scripts": { 4 | "build": "npx projen build", 5 | "bundle": "npx projen bundle", 6 | "clobber": "npx projen clobber", 7 | "compile": "npx projen compile", 8 | "default": "npx projen default", 9 | "deploy": "npx projen deploy", 10 | "destroy": "npx projen destroy", 11 | "diff": "npx projen diff", 12 | "eject": "npx projen eject", 13 | "eslint": "npx projen eslint", 14 | "package": "npx projen package", 15 | "post-compile": "npx projen post-compile", 16 | "post-upgrade": "npx projen post-upgrade", 17 | "pre-compile": "npx projen pre-compile", 18 | "synth": "npx projen synth", 19 | "synth:silent": "npx projen synth:silent", 20 | "test": "npx projen test", 21 | "test:watch": "npx projen test:watch", 22 | "upgrade": "npx projen upgrade", 23 | "watch": "npx projen watch", 24 | "projen": "npx projen" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^27.0.1", 28 | "@types/node": "^14", 29 | "@typescript-eslint/eslint-plugin": "^5", 30 | "@typescript-eslint/parser": "^5", 31 | "ansi-regex": "6.0.1", 32 | "aws-cdk": "^2.3.0", 33 | "esbuild": "^0.14.8", 34 | "eslint": "^8", 35 | "eslint-import-resolver-node": "^0.3.6", 36 | "eslint-import-resolver-typescript": "^2.4.0", 37 | "eslint-plugin-import": "^2.24.2", 38 | "jest": "^27.1.1", 39 | "jest-junit": "^13", 40 | "json-schema": "^0.4.0", 41 | "npm-check-updates": "^16", 42 | "projen": "^0.50.31", 43 | "ts-jest": "^27.0.5", 44 | "ts-node": "^9", 45 | "typescript": "^4.4.2" 46 | }, 47 | "dependencies": { 48 | "aws-cdk-lib": "^2.3.0", 49 | "cdk-bootstrapless-synthesizer": "^2", 50 | "constructs": "^10.0.5" 51 | }, 52 | "license": "UNLICENSED", 53 | "version": "0.0.0", 54 | "jest": { 55 | "testMatch": [ 56 | "/src/**/__tests__/**/*.ts?(x)", 57 | "/(test|src)/**/*(*.)@(spec|test).ts?(x)" 58 | ], 59 | "clearMocks": true, 60 | "collectCoverage": true, 61 | "coverageReporters": [ 62 | "json", 63 | "lcov", 64 | "clover", 65 | "cobertura", 66 | "text" 67 | ], 68 | "coverageDirectory": "coverage", 69 | "coveragePathIgnorePatterns": [ 70 | "/node_modules/" 71 | ], 72 | "testPathIgnorePatterns": [ 73 | "/node_modules/" 74 | ], 75 | "watchPathIgnorePatterns": [ 76 | "/node_modules/" 77 | ], 78 | "reporters": [ 79 | "default", 80 | [ 81 | "jest-junit", 82 | { 83 | "outputDirectory": "test-reports" 84 | } 85 | ] 86 | ], 87 | "preset": "ts-jest", 88 | "globals": { 89 | "ts-jest": { 90 | "tsconfig": "tsconfig.dev.json" 91 | } 92 | } 93 | }, 94 | "resolutions": { 95 | "minimatch": "3.0.5" 96 | }, 97 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 98 | } 99 | -------------------------------------------------------------------------------- /sample/src/main.ts: -------------------------------------------------------------------------------- 1 | /// !! # cdk-bootstrapless-synthesizer 2 | /// !! 3 | /// !! [![npm version](https://img.shields.io/npm/v/cdk-bootstrapless-synthesizer)](https://www.npmjs.com/package/cdk-bootstrapless-synthesizer) 4 | /// !! [![PyPI](https://img.shields.io/pypi/v/cdk-bootstrapless-synthesizer)](https://pypi.org/project/cdk-bootstrapless-synthesizer) 5 | /// !! [![npm](https://img.shields.io/npm/dw/cdk-bootstrapless-synthesizer?label=npm%20downloads)](https://www.npmjs.com/package/cdk-bootstrapless-synthesizer) 6 | /// !! [![PyPI - Downloads](https://img.shields.io/pypi/dw/cdk-bootstrapless-synthesizer?label=pypi%20downloads)](https://pypi.org/project/cdk-bootstrapless-synthesizer) 7 | /// !! 8 | /// !! A bootstrapless stack synthesizer that is designated to generate templates that can be directly used by AWS CloudFormation. 9 | /// !! 10 | /// !! Please use ^1.0.0 for cdk version 1.x.x, use ^2.0.0 for cdk version 2.x.x 11 | /// !! 12 | /// !! ## Usage 13 | 14 | import * as path from 'path'; 15 | import { App, CfnOutput, Stack, StackProps } from 'aws-cdk-lib'; 16 | import { Repository, RepositoryBase, IRepository } from 'aws-cdk-lib/aws-ecr'; 17 | import { DockerImageAsset, DockerImageAssetProps } from 'aws-cdk-lib/aws-ecr-assets'; 18 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 19 | import * as iam from 'aws-cdk-lib/aws-iam'; 20 | import { DockerImage, DockerImageConfig, ISageMakerTask } from 'aws-cdk-lib/aws-stepfunctions-tasks'; 21 | /// !show 22 | import { BootstraplessStackSynthesizer } from 'cdk-bootstrapless-synthesizer'; 23 | /// !hide 24 | import { Construct } from 'constructs'; 25 | 26 | const env = process.env; 27 | 28 | class StandardDockerImage extends DockerImage { 29 | private readonly allowAnyEcrImagePull: boolean; 30 | private readonly imageUri: string; 31 | private readonly repository?: IRepository; 32 | 33 | constructor(opts: { allowAnyEcrImagePull?: boolean; imageUri: string; repository?: IRepository }) { 34 | super(); 35 | 36 | this.allowAnyEcrImagePull = !!opts.allowAnyEcrImagePull; 37 | this.imageUri = opts.imageUri; 38 | this.repository = opts.repository; 39 | } 40 | 41 | public bind(task: ISageMakerTask): DockerImageConfig { 42 | if (this.repository) { 43 | this.repository.grantPull(task); 44 | } 45 | if (this.allowAnyEcrImagePull) { 46 | task.grantPrincipal.addToPrincipalPolicy(new iam.PolicyStatement({ 47 | actions: [ 48 | 'ecr:BatchCheckLayerAvailability', 49 | 'ecr:GetDownloadUrlForLayer', 50 | 'ecr:BatchGetImage', 51 | ], 52 | resources: ['*'], 53 | })); 54 | } 55 | return { 56 | imageUri: this.imageUri, 57 | }; 58 | } 59 | } 60 | 61 | function fromAsset(scope: Construct, id: string, props: DockerImageAssetProps): DockerImage { 62 | const asset = WithCrossAccount(new DockerImageAsset(scope, id, props)); 63 | return new StandardDockerImage({ repository: asset.repository, imageUri: asset.imageUri }); 64 | } 65 | 66 | export class MyStack extends Stack { 67 | constructor(scope: Construct, id: string, props: StackProps = {}) { 68 | super(scope, id, props); 69 | 70 | const image = WithCrossAccount(new DockerImageAsset(this, 'MyBuildImage', { 71 | directory: path.join(__dirname, '../docker'), 72 | })); 73 | 74 | 75 | new CfnOutput(this, 'output', { value: image.imageUri }); 76 | 77 | const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef'); 78 | taskDefinition.addContainer('DefaultContainer', { 79 | image: ecs.ContainerImage.fromDockerImageAsset(image), 80 | memoryLimitMiB: 512, 81 | }); 82 | 83 | fromAsset(this, 'stepfunctions', { 84 | directory: path.join(__dirname, '../docker'), 85 | }); 86 | } 87 | } 88 | 89 | /// !show 90 | const app = new App(); 91 | 92 | new MyStack(app, 'my-stack-dev', { 93 | synthesizer: new BootstraplessStackSynthesizer({ 94 | templateBucketName: 'cfn-template-bucket', 95 | 96 | fileAssetBucketName: 'file-asset-bucket-${AWS::Region}', 97 | fileAssetRegionSet: ['us-west-1', 'us-west-2'], 98 | fileAssetPrefix: 'file-asset-prefix/latest/', 99 | 100 | imageAssetRepositoryName: 'your-ecr-repo-name', 101 | imageAssetAccountId: '1234567890', 102 | imageAssetTagPrefix: 'latest-', 103 | imageAssetRegionSet: ['us-west-1', 'us-west-2'], 104 | }), 105 | }); 106 | 107 | // Or by environment variables 108 | env.BSS_TEMPLATE_BUCKET_NAME = 'cfn-template-bucket'; 109 | 110 | env.BSS_FILE_ASSET_BUCKET_NAME = 'file-asset-bucket-\${AWS::Region}'; 111 | env.BSS_FILE_ASSET_REGION_SET = 'us-west-1,us-west-2'; 112 | env.BSS_FILE_ASSET_PREFIX = 'file-asset-prefix/latest/'; 113 | 114 | env.BSS_IMAGE_ASSET_REPOSITORY_NAME = 'your-ecr-repo-name'; 115 | env.BSS_IMAGE_ASSET_ACCOUNT_ID = '1234567890'; 116 | env.BSS_IMAGE_ASSET_TAG_PREFIX = 'latest-'; 117 | env.BSS_IMAGE_ASSET_REGION_SET = 'us-west-1,us-west-2'; 118 | 119 | new MyStack(app, 'my-stack-dev2', { 120 | synthesizer: new BootstraplessStackSynthesizer(), 121 | }); 122 | 123 | // use Aspect to grant the role to pull ECR repository from account BSS_IMAGE_ASSET_ACCOUNT_ID 124 | /// !hide 125 | 126 | app.synth(); 127 | 128 | /// !! 129 | /// !! Synth AWS CloudFormation templates, assets and upload them 130 | /// !! 131 | /// !! ```shell 132 | /// !! $ cdk synth 133 | /// !! $ npx cdk-assets publish -p cdk.out/my-stack-dev.assets.json -v 134 | /// !! ``` 135 | 136 | /// !! ## Limitations 137 | /// !! When using `BSS_IMAGE_ASSET_ACCOUNT_ID` to push ECR repository to shared account, you need use `Aspect` to grant the role with policy to pull the repository from cross account. Or using the following `WithCrossAccount` techniques. 138 | /// !! 139 | /// !! Currently only below scenarios are supported, 140 | /// !! 141 | /// !! - ECS 142 | /// !! - SageMaker training job integrated with Step Functions 143 | /// !! - AWS Batch 144 | /// !! - AWS Lambda 145 | /// !! 146 | /// !! For other scenarios, the feature request or pull request are welcome. 147 | 148 | /// !show 149 | function OverrideRepositoryAccount(scope: Construct, id: string, repo: IRepository): IRepository { 150 | class Import extends RepositoryBase { 151 | public repositoryName = repo.repositoryName; 152 | public repositoryArn = Repository.arnForLocalRepository(repo.repositoryName, scope, env.BSS_IMAGE_ASSET_ACCOUNT_ID); 153 | 154 | public addToResourcePolicy(_statement: iam.PolicyStatement): iam.AddToResourcePolicyResult { 155 | // dropped 156 | return { statementAdded: false }; 157 | } 158 | } 159 | 160 | return new Import(scope, id); 161 | } 162 | 163 | function WithCrossAccount(image: DockerImageAsset): DockerImageAsset { 164 | image.repository = OverrideRepositoryAccount(image, 'CrossAccountRepo', image.repository); 165 | return image; 166 | } 167 | 168 | export class SampleStack extends Stack { 169 | constructor(scope: Construct, id: string, props: StackProps = {}) { 170 | super(scope, id, props); 171 | 172 | const image = WithCrossAccount(new DockerImageAsset(this, 'MyBuildImage', { 173 | directory: path.join(__dirname, '../docker'), 174 | })); 175 | 176 | new CfnOutput(this, 'output', { value: image.imageUri }); 177 | 178 | const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef'); 179 | taskDefinition.addContainer('DefaultContainer', { 180 | image: ecs.ContainerImage.fromDockerImageAsset(image), 181 | memoryLimitMiB: 512, 182 | }); 183 | 184 | fromAsset(this, 'stepfunctions', { 185 | directory: path.join(__dirname, '../docker'), 186 | }); 187 | } 188 | } 189 | /// !hide 190 | 191 | /// !! ## Sample Project 192 | /// !! 193 | /// !! See [Sample Project](./sample/README.md) 194 | /// !! 195 | /// !! ## API Reference 196 | /// !! 197 | /// !! See [API Reference](./API.md) for API details. -------------------------------------------------------------------------------- /sample/test/__snapshots__/main.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Snapshot 1`] = ` 4 | Object { 5 | "Outputs": Object { 6 | "output": Object { 7 | "Value": Object { 8 | "Fn::Sub": "\${AWS::AccountId}.dkr.ecr.\${AWS::Region}.\${AWS::URLSuffix}/cdk-hnb659fds-container-assets-\${AWS::AccountId}-\${AWS::Region}:d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3", 9 | }, 10 | }, 11 | }, 12 | "Parameters": Object { 13 | "BootstrapVersion": Object { 14 | "Default": "/cdk-bootstrap/hnb659fds/version", 15 | "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", 16 | "Type": "AWS::SSM::Parameter::Value", 17 | }, 18 | }, 19 | "Resources": Object { 20 | "TaskDef54694570": Object { 21 | "Properties": Object { 22 | "ContainerDefinitions": Array [ 23 | Object { 24 | "Essential": true, 25 | "Image": Object { 26 | "Fn::Sub": "\${AWS::AccountId}.dkr.ecr.\${AWS::Region}.\${AWS::URLSuffix}/cdk-hnb659fds-container-assets-\${AWS::AccountId}-\${AWS::Region}:d195bc4919540c304d368e3082c0a6103a3661c189ba1b4863162a31ecf4b8e3", 27 | }, 28 | "Memory": 512, 29 | "Name": "DefaultContainer", 30 | }, 31 | ], 32 | "Cpu": "256", 33 | "ExecutionRoleArn": Object { 34 | "Fn::GetAtt": Array [ 35 | "TaskDefExecutionRoleB4775C97", 36 | "Arn", 37 | ], 38 | }, 39 | "Family": "testTaskDefFA002F41", 40 | "Memory": "512", 41 | "NetworkMode": "awsvpc", 42 | "RequiresCompatibilities": Array [ 43 | "FARGATE", 44 | ], 45 | "TaskRoleArn": Object { 46 | "Fn::GetAtt": Array [ 47 | "TaskDefTaskRole1EDB4A67", 48 | "Arn", 49 | ], 50 | }, 51 | }, 52 | "Type": "AWS::ECS::TaskDefinition", 53 | }, 54 | "TaskDefExecutionRoleB4775C97": Object { 55 | "Properties": Object { 56 | "AssumeRolePolicyDocument": Object { 57 | "Statement": Array [ 58 | Object { 59 | "Action": "sts:AssumeRole", 60 | "Effect": "Allow", 61 | "Principal": Object { 62 | "Service": "ecs-tasks.amazonaws.com", 63 | }, 64 | }, 65 | ], 66 | "Version": "2012-10-17", 67 | }, 68 | }, 69 | "Type": "AWS::IAM::Role", 70 | }, 71 | "TaskDefExecutionRoleDefaultPolicy0DBB737A": Object { 72 | "Properties": Object { 73 | "PolicyDocument": Object { 74 | "Statement": Array [ 75 | Object { 76 | "Action": Array [ 77 | "ecr:BatchCheckLayerAvailability", 78 | "ecr:GetDownloadUrlForLayer", 79 | "ecr:BatchGetImage", 80 | ], 81 | "Effect": "Allow", 82 | "Resource": Object { 83 | "Fn::Join": Array [ 84 | "", 85 | Array [ 86 | "arn:", 87 | Object { 88 | "Ref": "AWS::Partition", 89 | }, 90 | ":ecr:", 91 | Object { 92 | "Ref": "AWS::Region", 93 | }, 94 | ":1234567890:repository/", 95 | Object { 96 | "Fn::Sub": "cdk-hnb659fds-container-assets-\${AWS::AccountId}-\${AWS::Region}", 97 | }, 98 | ], 99 | ], 100 | }, 101 | }, 102 | Object { 103 | "Action": "ecr:GetAuthorizationToken", 104 | "Effect": "Allow", 105 | "Resource": "*", 106 | }, 107 | ], 108 | "Version": "2012-10-17", 109 | }, 110 | "PolicyName": "TaskDefExecutionRoleDefaultPolicy0DBB737A", 111 | "Roles": Array [ 112 | Object { 113 | "Ref": "TaskDefExecutionRoleB4775C97", 114 | }, 115 | ], 116 | }, 117 | "Type": "AWS::IAM::Policy", 118 | }, 119 | "TaskDefTaskRole1EDB4A67": Object { 120 | "Properties": Object { 121 | "AssumeRolePolicyDocument": Object { 122 | "Statement": Array [ 123 | Object { 124 | "Action": "sts:AssumeRole", 125 | "Effect": "Allow", 126 | "Principal": Object { 127 | "Service": "ecs-tasks.amazonaws.com", 128 | }, 129 | }, 130 | ], 131 | "Version": "2012-10-17", 132 | }, 133 | }, 134 | "Type": "AWS::IAM::Role", 135 | }, 136 | }, 137 | "Rules": Object { 138 | "CheckBootstrapVersion": Object { 139 | "Assertions": Array [ 140 | Object { 141 | "Assert": Object { 142 | "Fn::Not": Array [ 143 | Object { 144 | "Fn::Contains": Array [ 145 | Array [ 146 | "1", 147 | "2", 148 | "3", 149 | "4", 150 | "5", 151 | ], 152 | Object { 153 | "Ref": "BootstrapVersion", 154 | }, 155 | ], 156 | }, 157 | ], 158 | }, 159 | "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", 160 | }, 161 | ], 162 | }, 163 | }, 164 | } 165 | `; 166 | -------------------------------------------------------------------------------- /sample/test/main.test.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'aws-cdk-lib'; 2 | import { MyStack } from '../src/main'; 3 | 4 | test('Snapshot', () => { 5 | const app = new App(); 6 | const stack = new MyStack(app, 'test'); 7 | 8 | expect(app.synth().getStackArtifact(stack.artifactId).template).toMatchSnapshot(); 9 | }); -------------------------------------------------------------------------------- /sample/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "inlineSourceMap": true, 8 | "inlineSources": true, 9 | "lib": [ 10 | "es2019" 11 | ], 12 | "module": "CommonJS", 13 | "noEmitOnError": false, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "noImplicitThis": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "resolveJsonModule": true, 21 | "strict": true, 22 | "strictNullChecks": true, 23 | "strictPropertyInitialization": true, 24 | "stripInternal": true, 25 | "target": "ES2019" 26 | }, 27 | "include": [ 28 | ".projenrc.js", 29 | "src/**/*.ts", 30 | "test/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ], 35 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 36 | } 37 | -------------------------------------------------------------------------------- /sample/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "lib", 5 | "alwaysStrict": true, 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "inlineSourceMap": true, 10 | "inlineSources": true, 11 | "lib": [ 12 | "es2019" 13 | ], 14 | "module": "CommonJS", 15 | "noEmitOnError": false, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitAny": true, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "strict": true, 24 | "strictNullChecks": true, 25 | "strictPropertyInitialization": true, 26 | "stripInternal": true, 27 | "target": "ES2019" 28 | }, 29 | "include": [ 30 | "src/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "cdk.out" 34 | ], 35 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 36 | } 37 | -------------------------------------------------------------------------------- /scripts/extractdoc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import re 4 | import sys 5 | import glob 6 | import textwrap 7 | import subprocess 8 | 9 | 10 | def sh(*args): 11 | return subprocess.check_output(*args, shell=True).decode() 12 | 13 | 14 | def cat(*s, delimiter='\n'): 15 | return delimiter.join(s) 16 | 17 | 18 | def renderf(filename, basedir=os.curdir): 19 | lang = os.path.splitext(filename)[1][1:] 20 | show = False 21 | code = [] 22 | body = [] 23 | with open(filename) as fp: 24 | for line in fp: 25 | if (not show) and re.search(r'^\s*/// !show', line, re.MULTILINE): 26 | show = True 27 | continue 28 | 29 | if show and re.search(r'^\s*/// !hide', line, re.MULTILINE): 30 | show = False 31 | if len(body): 32 | body.append(cat( 33 | f'```{lang}', 34 | textwrap.dedent(cat(*code)), 35 | '```', 36 | f'[{os.path.basename(filename)}]({os.path.relpath(filename, basedir)})', 37 | )) 38 | continue 39 | 40 | if show: 41 | code.append(line.rstrip()) 42 | continue 43 | else: 44 | code = [] 45 | 46 | if re.search(r'^\s*/// !tree', line, re.MULTILINE): 47 | body.append(cat( 48 | '```', 49 | sh(f'cd {os.path.dirname(filename)} && tree -L 3 -F .').rstrip(), 50 | '```', 51 | )) 52 | continue 53 | 54 | m = re.search(r'^\s*/// !!\s+(.*)', line, re.MULTILINE) 55 | if m: 56 | body.append(m.group(1)) 57 | continue 58 | 59 | return cat(*body) 60 | 61 | 62 | def _render(path, basedir=os.curdir, level=3): 63 | if os.path.isfile(path): 64 | yield renderf(path, basedir) 65 | if os.path.isdir(path) and (level > 0): 66 | for p in map(lambda f: os.path.join(path, f), sorted(os.listdir(path))): 67 | for s in _render(p, basedir, level-1): 68 | if s: 69 | yield s 70 | 71 | 72 | def render(path, basedir=os.curdir, level=3): 73 | return cat(*_render(path, basedir, level)) 74 | 75 | 76 | def main(): 77 | pathname = sys.argv[1] 78 | 79 | if sys.argv[2] == '-': 80 | fp = sys.stdout 81 | basedir = os.path.curdir 82 | else: 83 | name = os.path.abspath(sys.argv[2]) 84 | basedir = os.path.dirname(name) 85 | fp = open(name, 'w') 86 | 87 | for each in glob.iglob(pathname, recursive=True): 88 | fp.write(render(each, basedir)) 89 | fp.write('\n') 90 | 91 | fp.close() 92 | 93 | 94 | if __name__ == '__main__': 95 | main() 96 | -------------------------------------------------------------------------------- /src/aspect.ts: -------------------------------------------------------------------------------- 1 | import * as batch from '@aws-cdk/aws-batch-alpha'; 2 | import { IAspect, Stack, Arn } from 'aws-cdk-lib'; 3 | import * as batch_lib from 'aws-cdk-lib/aws-batch'; 4 | import { TaskDefinition, CfnTaskDefinition } from 'aws-cdk-lib/aws-ecs'; 5 | import { Policy, PolicyStatement, Role } from 'aws-cdk-lib/aws-iam'; 6 | import { SageMakerCreateTrainingJob } from 'aws-cdk-lib/aws-stepfunctions-tasks'; 7 | import { IConstruct } from 'constructs'; 8 | 9 | const FN_SUB = 'Fn::Sub'; 10 | /** 11 | * Configuration properties for ECRRepositoryAspect 12 | */ 13 | export interface ECRRepositoryAspectProps { 14 | /** 15 | * Override the ECR repository account id of the Docker Image assets 16 | * 17 | * @default - process.env.BSS_IMAGE_ASSET_ACCOUNT_ID 18 | */ 19 | readonly imageAssetAccountId?: string; 20 | } 21 | 22 | /** 23 | * Abtract aspect for ECR repository. 24 | * 25 | * You must provide the account id in props or set BSS_IMAGE_ASSET_ACCOUNT_ID in env 26 | */ 27 | export abstract class ECRRepositoryAspect implements IAspect { 28 | 29 | /** 30 | * @internal 31 | */ 32 | static readonly _repoPolicies = new Map(); 33 | readonly account: string; 34 | 35 | constructor(props: ECRRepositoryAspectProps = {}) { 36 | this.account = props.imageAssetAccountId ?? process.env.BSS_IMAGE_ASSET_ACCOUNT_ID!; 37 | } 38 | 39 | abstract visit(construct: IConstruct): void; 40 | 41 | protected getRepoName(imageUri: string): string | undefined { 42 | const matches = /\d{12}\.dkr\.ecr\..*\/(.*):.*/g.exec(imageUri); 43 | if (matches) { 44 | return matches[1]; 45 | } 46 | return undefined; 47 | } 48 | 49 | protected crossAccountECRPolicy(stack: Stack, repoName: string): Policy { 50 | const id = `${stack.stackName}-${repoName}`; 51 | const policy = ECRRepositoryAspect._repoPolicies.get(id); 52 | if (policy) { 53 | return policy; 54 | } 55 | 56 | const newPolicy = new Policy(stack, `CrossAccountECR-${repoName}`, { 57 | statements: [ 58 | new PolicyStatement({ 59 | actions: [ 60 | 'ecr:BatchCheckLayerAvailability', 61 | 'ecr:GetDownloadUrlForLayer', 62 | 'ecr:BatchGetImage', 63 | ], 64 | resources: [ 65 | Arn.format({ 66 | account: this.account, 67 | service: 'ecr', 68 | resource: 'repository', 69 | resourceName: repoName, 70 | }, stack), 71 | ], 72 | }), 73 | ], 74 | }); 75 | ECRRepositoryAspect._repoPolicies.set(id, newPolicy); 76 | return newPolicy; 77 | } 78 | } 79 | 80 | /** 81 | * Process the image assets in ECS task definition 82 | */ 83 | export class ECSTaskDefinition extends ECRRepositoryAspect { 84 | 85 | constructor(props: ECRRepositoryAspectProps = {}) { 86 | super(props); 87 | } 88 | 89 | protected hasBeReplaced(prop: CfnTaskDefinition.ContainerDefinitionProperty): string | undefined { 90 | if (typeof prop.image === 'object' && FN_SUB in prop.image && 91 | (prop.image[FN_SUB] as string).indexOf(this.account) > -1) { 92 | return prop.image[FN_SUB]; 93 | } else if (prop.image && (prop.image as string) && prop.image.indexOf(this.account) > -1) { 94 | return prop.image; 95 | } 96 | return undefined; 97 | } 98 | 99 | public visit(construct: IConstruct): void { 100 | if (construct instanceof TaskDefinition) { 101 | const containers = construct.stack.resolve((construct.node.defaultChild as CfnTaskDefinition).containerDefinitions); 102 | let imageUri = undefined; 103 | if (containers instanceof Array) { 104 | for (const container of containers) { 105 | if (container as CfnTaskDefinition.ContainerDefinitionProperty) { 106 | imageUri = this.hasBeReplaced(container); 107 | if (imageUri) { 108 | break; 109 | } 110 | } 111 | } 112 | } else if (containers as CfnTaskDefinition.ContainerDefinitionProperty) { 113 | imageUri = this.hasBeReplaced(containers); 114 | } 115 | 116 | if (imageUri) { 117 | const repoName = this.getRepoName(imageUri); 118 | if (repoName) { 119 | construct.executionRole!.attachInlinePolicy(this.crossAccountECRPolicy(construct.stack, repoName)); 120 | } 121 | } 122 | } 123 | } 124 | } 125 | 126 | /** 127 | * Process the image assets in SageMaker training job in Step Functions 128 | */ 129 | export class StepFunctionsSageMakerTrainingJob extends ECRRepositoryAspect { 130 | constructor(props: ECRRepositoryAspectProps = {}) { 131 | super(props); 132 | } 133 | 134 | public visit(construct: IConstruct): void { 135 | if (construct instanceof SageMakerCreateTrainingJob) { 136 | const stack = Stack.of(construct); 137 | const state = construct.toStateJson() as { 138 | Parameters: { 139 | AlgorithmSpecification: { 140 | TrainingImage: any; 141 | }; 142 | }; 143 | }; 144 | const image = stack.resolve(state.Parameters.AlgorithmSpecification.TrainingImage); 145 | if (FN_SUB in image) { 146 | const repoName = this.getRepoName(image[FN_SUB]); 147 | if (repoName) { 148 | construct.role.attachInlinePolicy(this.crossAccountECRPolicy(stack, repoName)); 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | 156 | /** 157 | * Process the image assets in AWS Batch job 158 | */ 159 | export class BatchJobDefinition extends ECRRepositoryAspect { 160 | /** 161 | * @internal 162 | */ 163 | readonly _repoNames: string[]; 164 | private _executionRole?: Role; 165 | private _executionRoleArn?: string; 166 | private _allRolesMap: Map; 167 | 168 | constructor(props: ECRRepositoryAspectProps = {}) { 169 | super(props); 170 | this._repoNames = []; 171 | this._allRolesMap = new Map(); 172 | } 173 | 174 | public visit(construct: IConstruct): void { 175 | if (construct instanceof batch.JobDefinition) { 176 | const stack = construct.stack; 177 | this._executionRoleArn = ((construct.node.defaultChild as batch_lib.CfnJobDefinition) 178 | .containerProperties as batch_lib.CfnJobDefinition.ContainerPropertiesProperty).executionRoleArn; 179 | 180 | if (this._executionRoleArn && this._allRolesMap.get(this._executionRoleArn)) { 181 | this._executionRole = this._allRolesMap.get(this._executionRoleArn); 182 | } 183 | const image = ((construct.node.defaultChild as batch_lib.CfnJobDefinition) 184 | .containerProperties as batch_lib.CfnJobDefinition.ContainerPropertiesProperty).image; 185 | const image_resolved = stack.resolve(image); 186 | if (FN_SUB in image_resolved) { 187 | const repoName = this.getRepoName(image_resolved[FN_SUB]); 188 | if (repoName) { 189 | if (this._executionRole) { 190 | this._executionRole.attachInlinePolicy(this.crossAccountECRPolicy(stack, repoName)); 191 | } else { 192 | if (this._repoNames.indexOf(repoName) < 0) { 193 | this._repoNames.push(repoName); 194 | } 195 | } 196 | } 197 | } 198 | } 199 | if (construct instanceof Role) { 200 | this._allRolesMap.set(construct.roleArn, construct); 201 | if (construct.roleArn == this._executionRoleArn) { 202 | const stack = construct.stack; 203 | this._executionRole = construct; 204 | while (this._repoNames.length > 0) { 205 | const repoName = this._repoNames.pop(); 206 | if (repoName) { 207 | this._executionRole.attachInlinePolicy(this.crossAccountECRPolicy(stack, repoName)); 208 | } 209 | } 210 | } 211 | } 212 | } 213 | } 214 | 215 | 216 | /** 217 | * Default ECR asset aspect, support using ECR assets in below services, 218 | * 219 | * - ECS task definition 220 | * - SageMaker training job in Step Functions 221 | * - AWS Batch job 222 | * - AWS Lambda container image 223 | */ 224 | export class CompositeECRRepositoryAspect extends ECRRepositoryAspect { 225 | 226 | /** 227 | * @internal 228 | */ 229 | readonly _aspects: ECRRepositoryAspect[]; 230 | 231 | constructor(props: ECRRepositoryAspectProps = {}) { 232 | super(props); 233 | this._aspects = [ 234 | new ECSTaskDefinition(props), 235 | new StepFunctionsSageMakerTrainingJob(props), 236 | new BatchJobDefinition(props), 237 | ]; 238 | } 239 | 240 | visit(construct: IConstruct): void { 241 | for (const _aspect of this._aspects) { 242 | _aspect.visit(construct); 243 | } 244 | } 245 | } -------------------------------------------------------------------------------- /src/template.yml: -------------------------------------------------------------------------------- 1 | Metadata: 2 | AWS::CloudFormation::Interface: 3 | ParameterGroups: 4 | - Label: 5 | default: Configuration 6 | Parameters: 7 | - Qualifier 8 | - TrustedAccounts 9 | - FileAssetsBuckets 10 | - ImageAssetsRepositories 11 | 12 | Parameters: 13 | Qualifier: 14 | Description: An identifier to distinguish multiple bootstrapless stacks in the same environment 15 | Default: my-identifier 16 | Type: String 17 | TrustedAccounts: 18 | Description: List of AWS accounts that are trusted to publish assets and deploy stacks to this environment 19 | Default: ", , ..." 20 | Type: CommaDelimitedList 21 | FileAssetsBuckets: 22 | Description: File assets buckets arn list separated by commas 23 | Default: "arn:aws:s3:::, arn:aws:s3:::/*, ..." 24 | Type: CommaDelimitedList 25 | ImageAssetsRepositories: 26 | Description: Image assets repositories arn list separated by commas 27 | Default: "arn:aws:ecr:::repository/, ..." 28 | Type: CommaDelimitedList 29 | 30 | Conditions: 31 | HasTrustedAccounts: 32 | Fn::Not: 33 | - Fn::Equals: 34 | - "" 35 | - Fn::Join: 36 | - "" 37 | - Ref: TrustedAccounts 38 | 39 | Resources: 40 | FilePublishingRole: 41 | Type: AWS::IAM::Role 42 | Properties: 43 | AssumeRolePolicyDocument: 44 | Statement: 45 | - Action: sts:AssumeRole 46 | Effect: Allow 47 | Principal: 48 | AWS: 49 | Ref: AWS::AccountId 50 | - Fn::If: 51 | - HasTrustedAccounts 52 | - Action: sts:AssumeRole 53 | Effect: Allow 54 | Principal: 55 | AWS: 56 | Ref: TrustedAccounts 57 | - Ref: AWS::NoValue 58 | RoleName: 59 | Fn::Sub: ${Qualifier}-file-publishing-role 60 | ImagePublishingRole: 61 | Type: AWS::IAM::Role 62 | Properties: 63 | AssumeRolePolicyDocument: 64 | Statement: 65 | - Action: sts:AssumeRole 66 | Effect: Allow 67 | Principal: 68 | AWS: 69 | Ref: AWS::AccountId 70 | - Fn::If: 71 | - HasTrustedAccounts 72 | - Action: sts:AssumeRole 73 | Effect: Allow 74 | Principal: 75 | AWS: 76 | Ref: TrustedAccounts 77 | - Ref: AWS::NoValue 78 | RoleName: 79 | Fn::Sub: ${Qualifier}-image-publishing-role 80 | FilePublishingRoleDefaultPolicy: 81 | Type: AWS::IAM::Policy 82 | Properties: 83 | PolicyDocument: 84 | Statement: 85 | - Action: 86 | - s3:GetObject* 87 | - s3:GetBucket* 88 | - s3:List* 89 | - s3:DeleteObject* 90 | - s3:PutObject* 91 | - s3:Abort* 92 | Resource: !Ref FileAssetsBuckets 93 | Effect: Allow 94 | Version: "2012-10-17" 95 | Roles: 96 | - Ref: FilePublishingRole 97 | PolicyName: 98 | Fn::Sub: ${Qualifier}-file-publishing-role-default-policy 99 | ImagePublishingRoleDefaultPolicy: 100 | Type: AWS::IAM::Policy 101 | Properties: 102 | PolicyDocument: 103 | Statement: 104 | - Action: 105 | - ecr:PutImage 106 | - ecr:InitiateLayerUpload 107 | - ecr:UploadLayerPart 108 | - ecr:CompleteLayerUpload 109 | - ecr:BatchCheckLayerAvailability 110 | - ecr:DescribeRepositories 111 | - ecr:DescribeImages 112 | Resource: !Ref ImageAssetsRepositories 113 | Effect: Allow 114 | - Action: 115 | - ecr:GetAuthorizationToken 116 | Resource: "*" 117 | Effect: Allow 118 | Version: "2012-10-17" 119 | Roles: 120 | - Ref: ImagePublishingRole 121 | PolicyName: 122 | Fn::Sub: ${Qualifier}-image-publishing-role-default-policy 123 | 124 | Outputs: 125 | FilePublishingRoleArn: 126 | Value: !Sub ${FilePublishingRole.Arn} 127 | ImagePublishingRole: 128 | Value: !Sub ${ImagePublishingRole.Arn} -------------------------------------------------------------------------------- /test/BootstraplessStackSynthesizer.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema'; 3 | import { FileAssetPackaging, Stack, App } from 'aws-cdk-lib/core'; 4 | import * as cxapi from 'aws-cdk-lib/cx-api'; 5 | import { BootstraplessStackSynthesizer, BootstraplessStackSynthesizerProps, ImageAssetTagSuffixType } from '../src'; 6 | 7 | beforeEach(() => { 8 | delete process.env.BSS_FILE_ASSET_BUCKET_NAME; 9 | delete process.env.BSS_IMAGE_ASSET_REPOSITORY_NAME; 10 | delete process.env.BSS_FILE_ASSET_PUBLISHING_ROLE_ARN; 11 | delete process.env.BSS_IMAGE_ASSET_PUBLISHING_ROLE_ARN; 12 | delete process.env.BSS_FILE_ASSET_PREFIX; 13 | delete process.env.BSS_FILE_ASSET_REGION_SET; 14 | delete process.env.BSS_TEMPLATE_BUCKET_NAME; 15 | delete process.env.BSS_IMAGE_ASSET_TAG_PREFIX; 16 | delete process.env.BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE; 17 | delete process.env.BSS_IMAGE_ASSET_REGION_SET; 18 | delete process.env.BSS_IMAGE_ASSET_ACCOUNT_ID; 19 | }); 20 | 21 | test('default manifest json', () => { 22 | const stack = new Stack(); 23 | const synthesizer = new BootstraplessStackSynthesizer(); 24 | synthesizer.bind(stack); 25 | const json = JSON.parse(synthesizer.dumps()); 26 | 27 | expect(json.files).toEqual({}); 28 | expect(json.dockerImages).toEqual({}); 29 | }); 30 | 31 | test('assertBound for stack', () => { 32 | const synthesizer = new BootstraplessStackSynthesizer(); 33 | 34 | expect(() => { 35 | synthesizer.addFileAsset({ 36 | fileName: __filename, 37 | packaging: FileAssetPackaging.FILE, 38 | sourceHash: 'abcdef', 39 | }); 40 | }).toThrow('You must call bind'); 41 | }); 42 | 43 | test('assertBound for bucketName and repositoryName', () => { 44 | const stack = new Stack(); 45 | const synthesizer = new BootstraplessStackSynthesizer(); 46 | synthesizer.bind(stack); 47 | 48 | expect(() => { 49 | synthesizer.addFileAsset({ 50 | fileName: __filename, 51 | packaging: FileAssetPackaging.FILE, 52 | sourceHash: 'abcdef', 53 | }); 54 | }).toThrow('The bucketName is null'); 55 | 56 | expect(() => { 57 | synthesizer.addDockerImageAsset({ 58 | directoryName: __dirname, 59 | sourceHash: 'abcdef', 60 | }); 61 | }).toThrow('The repositoryName is null'); 62 | }); 63 | 64 | 65 | test('addFileAsset', () => { 66 | const stack = new Stack(); 67 | const synthesizer = new BootstraplessStackSynthesizer({ 68 | fileAssetBucketName: 'test-bucket-name', 69 | }); 70 | synthesizer.bind(stack); 71 | synthesizer.addFileAsset({ 72 | fileName: __filename, 73 | packaging: FileAssetPackaging.FILE, 74 | sourceHash: 'abcdef', 75 | }); 76 | const json = JSON.parse(synthesizer.dumps()); 77 | 78 | expect(json.files.abcdef.source).toEqual({ 79 | path: __filename, 80 | packaging: FileAssetPackaging.FILE, 81 | }); 82 | expect(json.files.abcdef.destinations).toEqual({ 83 | 'current_account-current_region': { 84 | bucketName: 'test-bucket-name', 85 | objectKey: 'abcdef', 86 | }, 87 | }); 88 | expect(json.dockerImages).toEqual({}); 89 | }); 90 | 91 | test.each([ 92 | () => mksynthzer({ fileAssetBucketName: 'test-bucket-name', fileAssetPrefix: 'test-prefix/' }), 93 | () => mksynthzer({ fileAssetBucketName: 'test-bucket-name' }, { BSS_FILE_ASSET_PREFIX: 'test-prefix/' }), 94 | ])('#%# addFileAsset when fileAssetsPrefix is set', (mksynthzerFn) => { 95 | const stack = new Stack(); 96 | const synthesizer = mksynthzerFn(); 97 | synthesizer.bind(stack); 98 | synthesizer.addFileAsset({ 99 | fileName: __filename, 100 | packaging: FileAssetPackaging.FILE, 101 | sourceHash: 'abcdef', 102 | }); 103 | const json = JSON.parse(synthesizer.dumps()); 104 | 105 | expect(json.files.abcdef.source).toEqual({ 106 | path: __filename, 107 | packaging: FileAssetPackaging.FILE, 108 | }); 109 | expect(json.files.abcdef.destinations).toEqual({ 110 | 'current_account-current_region': { 111 | bucketName: 'test-bucket-name', 112 | objectKey: 'test-prefix/abcdef', 113 | }, 114 | }); 115 | expect(json.dockerImages).toEqual({}); 116 | }); 117 | 118 | 119 | test.each([ 120 | () => mksynthzer({ fileAssetBucketName: 'test-bucket-name', fileAssetRegionSet: ['us-east-1', 'us-west-1', '', ' '] }), 121 | () => mksynthzer({ fileAssetBucketName: 'test-bucket-name' }, { BSS_FILE_ASSET_REGION_SET: 'us-east-1, us-west-1, , ,' }), 122 | ])('#%# addFileAsset when fileAssetsRegionSet is set but fileAssetsBucketName doesn\'t contains ${AWS::Region}', (mksynthzerFn) => { 123 | const stack = new Stack(); 124 | const synthesizer = mksynthzerFn(); 125 | synthesizer.bind(stack); 126 | synthesizer.addFileAsset({ 127 | fileName: __filename, 128 | packaging: FileAssetPackaging.FILE, 129 | sourceHash: 'abcdef', 130 | }); 131 | const json = JSON.parse(synthesizer.dumps()); 132 | 133 | 134 | expect(json.files.abcdef.source).toEqual({ 135 | path: __filename, 136 | packaging: FileAssetPackaging.FILE, 137 | }); 138 | expect(json.files.abcdef.destinations).toEqual({ 139 | 'current_account-current_region': { 140 | bucketName: 'test-bucket-name', 141 | objectKey: 'abcdef', 142 | region: 'us-east-1', 143 | }, 144 | }); 145 | expect(json.dockerImages).toEqual({}); 146 | }); 147 | 148 | test.each([ 149 | () => mksynthzer({ fileAssetBucketName: 'test-bucket-name-${AWS::Region}', fileAssetRegionSet: ['us-east-1', 'us-west-1', '', ' '] }), 150 | () => mksynthzer({ fileAssetBucketName: 'test-bucket-name-${AWS::Region}' }, { BSS_FILE_ASSET_REGION_SET: 'us-east-1, us-west-1, , ,' }), 151 | ])('#%# addFileAsset when fileAssetsRegionSet is set and fileAssetsBucketName contains ${AWS::Region}', (mksynthzerFn) => { 152 | const stack = new Stack(); 153 | const synthesizer = mksynthzerFn(); 154 | synthesizer.bind(stack); 155 | const location = synthesizer.addFileAsset({ 156 | fileName: __filename, 157 | packaging: FileAssetPackaging.FILE, 158 | sourceHash: 'abcdef', 159 | }); 160 | const json = JSON.parse(synthesizer.dumps()); 161 | 162 | 163 | expect(stack.resolve(location.s3ObjectUrl)).toEqual({ 'Fn::Sub': 's3://test-bucket-name-${AWS::Region}/abcdef' }); 164 | expect(stack.resolve(location.bucketName)).toEqual({ 'Fn::Sub': 'test-bucket-name-${AWS::Region}' }); 165 | expect(json.files.abcdef.source).toEqual({ 166 | path: __filename, 167 | packaging: FileAssetPackaging.FILE, 168 | }); 169 | expect(json.files.abcdef.destinations).toEqual({ 170 | 'us-east-1': { 171 | bucketName: 'test-bucket-name-us-east-1', 172 | objectKey: 'abcdef', 173 | region: 'us-east-1', 174 | }, 175 | 'us-west-1': { 176 | bucketName: 'test-bucket-name-us-west-1', 177 | objectKey: 'abcdef', 178 | region: 'us-west-1', 179 | }, 180 | }); 181 | expect(json.dockerImages).toEqual({}); 182 | }); 183 | 184 | 185 | test.each([ 186 | () => mksynthzer({ fileAssetBucketName: 'test-bucket-name-${AWS::Region}' }), 187 | () => mksynthzer({}, { BSS_FILE_ASSET_BUCKET_NAME: 'test-bucket-name-${AWS::Region}' }), 188 | ])('#%# addFileAsset when fileAssetsBucketName contains ${AWS::Region}', (mksynthzerFn) => { 189 | const stack = new Stack(); 190 | const synthesizer = mksynthzerFn(); 191 | synthesizer.bind(stack); 192 | const location = synthesizer.addFileAsset({ 193 | fileName: __filename, 194 | packaging: FileAssetPackaging.FILE, 195 | sourceHash: 'abcdef', 196 | }); 197 | const json = JSON.parse(synthesizer.dumps()); 198 | 199 | 200 | expect(stack.resolve(location.s3ObjectUrl)).toEqual({ 'Fn::Sub': 's3://test-bucket-name-${AWS::Region}/abcdef' }); 201 | expect(stack.resolve(location.bucketName)).toEqual({ 'Fn::Sub': 'test-bucket-name-${AWS::Region}' }); 202 | expect(json.files.abcdef.source).toEqual({ 203 | path: __filename, 204 | packaging: FileAssetPackaging.FILE, 205 | }); 206 | expect(json.files.abcdef.destinations).toEqual({ 207 | 'current_account-current_region': { 208 | bucketName: 'test-bucket-name-${AWS::Region}', 209 | objectKey: 'abcdef', 210 | }, 211 | }); 212 | expect(json.dockerImages).toEqual({}); 213 | }); 214 | 215 | 216 | test.each([ 217 | () => mksynthzer({ imageAssetRepositoryName: 'the-repo' }), 218 | () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo' }), 219 | ])('#%# addDockerImageAsset', (mksynthzerFn) => { 220 | const stack = new Stack(); 221 | const synthesizer = mksynthzerFn(); 222 | synthesizer.bind(stack); 223 | synthesizer.addDockerImageAsset({ 224 | directoryName: __dirname, 225 | sourceHash: 'abcdef', 226 | }); 227 | const json = JSON.parse(synthesizer.dumps()); 228 | 229 | expect(json.dockerImages.abcdef.source).toEqual({ 230 | directory: __dirname, 231 | }); 232 | expect(json.dockerImages.abcdef.destinations).toEqual({ 233 | 'current_account-current_region': { 234 | repositoryName: 'the-repo', 235 | imageTag: 'abcdef', 236 | }, 237 | }); 238 | expect(json.files).toEqual({}); 239 | }); 240 | 241 | 242 | test.each([ 243 | () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetTagPrefix: 'latest-' }), 244 | () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_TAG_PREFIX: 'latest-' }), 245 | ])('#%# addDockerImageAsset when imageAssetTagPrefix is specified', (mksynthzerFn) => { 246 | const stack = new Stack(); 247 | const synthesizer = mksynthzerFn(); 248 | synthesizer.bind(stack); 249 | synthesizer.addDockerImageAsset({ 250 | directoryName: __dirname, 251 | sourceHash: 'abcdef', 252 | }); 253 | const json = JSON.parse(synthesizer.dumps()); 254 | 255 | expect(json.dockerImages.abcdef.source).toEqual({ 256 | directory: __dirname, 257 | }); 258 | expect(json.dockerImages.abcdef.destinations).toEqual({ 259 | 'current_account-current_region': { 260 | repositoryName: 'the-repo', 261 | imageTag: 'latest-abcdef', 262 | }, 263 | }); 264 | expect(json.files).toEqual({}); 265 | }); 266 | 267 | test.each([ 268 | () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetTagSuffixType: ImageAssetTagSuffixType.HASH }), 269 | () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE: 'HASH' }), 270 | ])('#%# addDockerImageAsset when imageAssetTagSuffixType HASH is specified', (mksynthzerFn) => { 271 | const stack = new Stack(); 272 | const synthesizer = mksynthzerFn(); 273 | synthesizer.bind(stack); 274 | synthesizer.addDockerImageAsset({ 275 | directoryName: __dirname, 276 | sourceHash: 'abcdef', 277 | }); 278 | const json = JSON.parse(synthesizer.dumps()); 279 | 280 | expect(json.dockerImages.abcdef.source).toEqual({ 281 | directory: __dirname, 282 | }); 283 | expect(json.dockerImages.abcdef.destinations).toEqual({ 284 | 'current_account-current_region': { 285 | repositoryName: 'the-repo', 286 | imageTag: 'abcdef', 287 | }, 288 | }); 289 | expect(json.files).toEqual({}); 290 | }); 291 | 292 | test.each([ 293 | () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetTagSuffixType: ImageAssetTagSuffixType.NONE }), 294 | () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE: 'NONE' }), 295 | ])('#%# addDockerImageAsset when imageAssetTagSuffixType NONE is specified', (mksynthzerFn) => { 296 | const stack = new Stack(); 297 | const synthesizer = mksynthzerFn(); 298 | synthesizer.bind(stack); 299 | synthesizer.addDockerImageAsset({ 300 | directoryName: __dirname, 301 | sourceHash: 'abcdef', 302 | }); 303 | const json = JSON.parse(synthesizer.dumps()); 304 | 305 | expect(json.dockerImages.abcdef.source).toEqual({ 306 | directory: __dirname, 307 | }); 308 | expect(json.dockerImages.abcdef.destinations).toEqual({ 309 | 'current_account-current_region': { 310 | repositoryName: 'the-repo', 311 | imageTag: '', 312 | }, 313 | }); 314 | expect(json.files).toEqual({}); 315 | }); 316 | 317 | test.each([ 318 | () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetTagSuffixType: 'invalid' as ImageAssetTagSuffixType }), 319 | () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE: 'INVALID' }), 320 | () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_TAG_SUFFIX_TYPE: '' }), 321 | ])('#%# addDockerImageAsset when imageAssetTagSuffixType is invalid', (mksynthzerFn) => { 322 | expect(() => { 323 | mksynthzerFn(); 324 | }).toThrow(/Invalid ImageAssetTagSuffixType/); 325 | }); 326 | 327 | 328 | test.each([ 329 | () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetRegionSet: ['us-west-1'] }), 330 | () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_REGION_SET: 'us-west-1' }), 331 | ])('#%# addDockerImageAsset when imageAssetsRegionSet is specified', (mksynthzerFn) => { 332 | const stack = new Stack(); 333 | const synthesizer = mksynthzerFn(); 334 | synthesizer.bind(stack); 335 | const location = synthesizer.addDockerImageAsset({ 336 | directoryName: __dirname, 337 | sourceHash: 'abcdef', 338 | }); 339 | const json = JSON.parse(synthesizer.dumps()); 340 | 341 | expect(stack.resolve(location.imageUri)).toEqual({ 342 | 'Fn::Sub': '${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/the-repo:abcdef', 343 | }); 344 | expect(json.dockerImages.abcdef.source).toEqual({ 345 | directory: __dirname, 346 | }); 347 | expect(json.dockerImages.abcdef.destinations).toEqual({ 348 | 'us-west-1': { 349 | repositoryName: 'the-repo', 350 | imageTag: 'abcdef', 351 | region: 'us-west-1', 352 | }, 353 | }); 354 | expect(json.files).toEqual({}); 355 | }); 356 | 357 | test.each([ 358 | () => mksynthzer({ imageAssetRepositoryName: 'the-repo', imageAssetAccountId: '1234567890' }), 359 | () => mksynthzer({}, { BSS_IMAGE_ASSET_REPOSITORY_NAME: 'the-repo', BSS_IMAGE_ASSET_ACCOUNT_ID: '1234567890' }), 360 | ])('#%# addDockerImageAsset when imageAssetsAccountId is specified', (mksynthzerFn) => { 361 | const stack = new Stack(); 362 | const synthesizer = mksynthzerFn(); 363 | synthesizer.bind(stack); 364 | const location = synthesizer.addDockerImageAsset({ 365 | directoryName: __dirname, 366 | sourceHash: 'abcdef', 367 | }); 368 | const json = JSON.parse(synthesizer.dumps()); 369 | 370 | expect(stack.resolve(location.imageUri)).toEqual({ 371 | 'Fn::Sub': '1234567890.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/the-repo:abcdef', 372 | }); 373 | expect(json.dockerImages.abcdef.source).toEqual({ 374 | directory: __dirname, 375 | }); 376 | expect(json.dockerImages.abcdef.destinations).toEqual({ 377 | 'current_account-current_region': { 378 | repositoryName: 'the-repo', 379 | imageTag: 'abcdef', 380 | }, 381 | }); 382 | expect(json.files).toEqual({}); 383 | }); 384 | 385 | test('synth', () => { 386 | const myapp = new App(); 387 | const mystack = new Stack(myapp, 'mystack', { 388 | synthesizer: new BootstraplessStackSynthesizer({ 389 | fileAssetBucketName: 'file-asset-bucket', 390 | fileAssetPublishingRoleArn: 'file:role:arn', 391 | templateBucketName: 'template-bucket', 392 | 393 | imageAssetRepositoryName: 'image-ecr-repository', 394 | imageAssetPublishingRoleArn: 'image:role:arn', 395 | }), 396 | }); 397 | 398 | mystack.synthesizer.addFileAsset({ 399 | fileName: __filename, 400 | packaging: FileAssetPackaging.FILE, 401 | sourceHash: 'file-asset-hash', 402 | }); 403 | mystack.synthesizer.addDockerImageAsset({ 404 | directoryName: __dirname, 405 | sourceHash: 'docker-asset-hash', 406 | }); 407 | const asm = myapp.synth(); 408 | const manifest = readAssetManifest(asm); 409 | 410 | expect(manifest.files?.['mystack.template.json']?.source).toEqual({ 411 | path: 'mystack.template.json', 412 | packaging: 'file', 413 | }); 414 | expect(manifest.files?.['mystack.template.json']?.destinations).toEqual({ 415 | 'current_account-current_region': { 416 | bucketName: 'template-bucket', 417 | objectKey: 'mystack.template.json', 418 | assumeRoleArn: 'file:role:arn', 419 | }, 420 | }); 421 | expect(manifest.files?.['file-asset-hash']?.source).toEqual({ 422 | path: __filename, 423 | packaging: FileAssetPackaging.FILE, 424 | }); 425 | expect(manifest.files?.['file-asset-hash']?.destinations).toEqual({ 426 | 'current_account-current_region': { 427 | bucketName: 'file-asset-bucket', 428 | objectKey: 'file-asset-hash', 429 | assumeRoleArn: 'file:role:arn', 430 | }, 431 | }); 432 | expect(manifest.dockerImages?.['docker-asset-hash']?.source).toEqual({ 433 | directory: __dirname, 434 | }); 435 | expect(manifest.dockerImages?.['docker-asset-hash']?.destinations).toEqual({ 436 | 'current_account-current_region': { 437 | repositoryName: 'image-ecr-repository', 438 | imageTag: 'docker-asset-hash', 439 | assumeRoleArn: 'image:role:arn', 440 | }, 441 | }); 442 | }); 443 | 444 | // const CFN_CONTEXT = { 445 | // 'AWS::Region': 'the_region', 446 | // 'AWS::AccountId': 'the_account', 447 | // 'AWS::URLSuffix': 'domain.aws', 448 | // }; 449 | 450 | 451 | function isAssetManifest(x: cxapi.CloudArtifact): x is cxapi.AssetManifestArtifact { 452 | return x instanceof cxapi.AssetManifestArtifact; 453 | } 454 | 455 | 456 | function readAssetManifest(asm: cxapi.CloudAssembly): cxschema.AssetManifest { 457 | const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0]; 458 | if (!manifestArtifact) { throw new Error('no asset manifest in assembly'); } 459 | 460 | return JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' })); 461 | } 462 | 463 | function mksynthzer(props: BootstraplessStackSynthesizerProps, env?: { [key: string]: string }): BootstraplessStackSynthesizer { 464 | if (env) { 465 | for (const key in env) { 466 | process.env[key] = env[key]; 467 | } 468 | } 469 | return new BootstraplessStackSynthesizer(props); 470 | } 471 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | export function evaluateCFN(object: any, context: {[key: string]: string} = {}): any { 2 | const intrinsicFns: any = { 3 | 'Fn::Join'(separator: string, args: string[]) { 4 | if (typeof separator !== 'string') { 5 | // CFN does not support expressions here! 6 | throw new Error('\'separator\' argument of { Fn::Join } must be a string literal'); 7 | } 8 | return evaluate(args).map(evaluate).join(separator); 9 | }, 10 | 11 | 'Fn::Split'(separator: string, args: any) { 12 | if (typeof separator !== 'string') { 13 | // CFN does not support expressions here! 14 | throw new Error('\'separator\' argument of { Fn::Split } must be a string literal'); 15 | } 16 | return evaluate(args).split(separator); 17 | }, 18 | 19 | 'Fn::Select'(index: number, args: any) { 20 | return evaluate(args).map(evaluate)[index]; 21 | }, 22 | 23 | 'Ref'(logicalId: string) { 24 | if (!(logicalId in context)) { 25 | throw new Error(`Trying to evaluate Ref of '${logicalId}' but not in context!`); 26 | } 27 | return context[logicalId]; 28 | }, 29 | 30 | 'Fn::GetAtt'(logicalId: string, attributeName: string) { 31 | const key = `${logicalId}.${attributeName}`; 32 | if (!(key in context)) { 33 | throw new Error(`Trying to evaluate Fn::GetAtt of '${logicalId}.${attributeName}' but not in context!`); 34 | } 35 | return context[key]; 36 | }, 37 | 38 | 'Fn::Sub'(argument: string | [string, Record]) { 39 | let template; 40 | let placeholders: Record; 41 | if (Array.isArray(argument)) { 42 | template = argument[0]; 43 | placeholders = evaluate(argument[1]); 44 | } else { 45 | template = argument; 46 | placeholders = context; 47 | } 48 | 49 | if (typeof template !== 'string') { 50 | throw new Error('The first argument to {Fn::Sub} must be a string literal (cannot be the result of an expression)'); 51 | } 52 | 53 | return template.replace(/\$\{([a-zA-Z0-9.:-]*)\}/g, (_: string, key: string) => { 54 | if (key in placeholders) { return placeholders[key]; } 55 | throw new Error(`Unknown placeholder in Fn::Sub: ${key}`); 56 | }); 57 | }, 58 | }; 59 | 60 | return evaluate(object); 61 | 62 | function evaluate(obj: any): any { 63 | if (Array.isArray(obj)) { 64 | return obj.map(evaluate); 65 | } 66 | 67 | if (typeof obj === 'object') { 68 | const intrinsic = parseIntrinsic(obj); 69 | if (intrinsic) { 70 | return evaluateIntrinsic(intrinsic); 71 | } 72 | 73 | const ret: {[key: string]: any} = {}; 74 | for (const key of Object.keys(obj)) { 75 | ret[key] = evaluateCFN(obj[key]); 76 | } 77 | return ret; 78 | } 79 | 80 | return obj; 81 | } 82 | 83 | function evaluateIntrinsic(intrinsic: Intrinsic) { 84 | if (!(intrinsic.name in intrinsicFns)) { 85 | throw new Error(`Intrinsic ${intrinsic.name} not supported here`); 86 | } 87 | 88 | const argsAsArray = Array.isArray(intrinsic.args) ? intrinsic.args : [intrinsic.args]; 89 | 90 | return intrinsicFns[intrinsic.name].apply(intrinsicFns, argsAsArray); 91 | } 92 | } 93 | 94 | interface Intrinsic { 95 | readonly name: string; 96 | readonly args: any; 97 | } 98 | 99 | function parseIntrinsic(x: any): Intrinsic | undefined { 100 | if (typeof x !== 'object' || x === null) { return undefined; } 101 | const keys = Object.keys(x); 102 | if (keys.length === 1 && (isNameOfCloudFormationIntrinsic(keys[0]) || keys[0] === 'Ref')) { 103 | return { 104 | name: keys[0], 105 | args: x[keys[0]], 106 | }; 107 | } 108 | return undefined; 109 | } 110 | 111 | function isNameOfCloudFormationIntrinsic(name: string): boolean { 112 | if (!name.startsWith('Fn::')) { 113 | return false; 114 | } 115 | // these are 'fake' intrinsics, only usable inside the parameter overrides of a CFN CodePipeline Action 116 | return name !== 'Fn::GetArtifactAtt' && name !== 'Fn::GetParam'; 117 | } -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "inlineSourceMap": true, 8 | "inlineSources": true, 9 | "lib": [ 10 | "es2019" 11 | ], 12 | "module": "CommonJS", 13 | "noEmitOnError": false, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "noImplicitThis": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "resolveJsonModule": true, 21 | "strict": true, 22 | "strictNullChecks": true, 23 | "strictPropertyInitialization": true, 24 | "stripInternal": true, 25 | "target": "ES2019" 26 | }, 27 | "include": [ 28 | ".projenrc.js", 29 | "src/**/*.ts", 30 | "test/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ], 35 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 36 | } 37 | --------------------------------------------------------------------------------