├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── auto-queue.yml │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ ├── upgrade-cdklabs-projen-project-types-main.yml │ ├── upgrade-dev-deps-main.yml │ └── upgrade-main.yml ├── .gitignore ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.ts ├── API.md ├── LICENSE ├── NOTICE ├── README.md ├── THIRD-PARTY-LICENSES.txt ├── build-lambda.sh ├── lambda-bin └── .gitkeep ├── lambda-src ├── .dockerignore ├── Dockerfile ├── Makefile ├── go.mod ├── go.sum ├── internal │ ├── iolimits │ │ ├── iolimits.go │ │ └── iolimits_test.go │ └── tarfile │ │ ├── reader.go │ │ ├── reader_test.go │ │ ├── s3file.go │ │ ├── s3file_test.go │ │ ├── src.go │ │ └── types.go ├── main.go ├── main_test.go ├── s3 │ ├── src.go │ ├── transport.go │ └── transport_test.go ├── utils.go └── utils_test.go ├── package.json ├── rosetta └── default.ts-fixture ├── src └── index.ts ├── test ├── docker │ └── Dockerfile ├── dockerhub-example.ecr-deployment.ts ├── ecr-deployment.test.ts ├── example.ecr-deployment.ts └── index.test.ts ├── tsconfig.dev.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import", 11 | "@stylistic" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaVersion": 2018, 16 | "sourceType": "module", 17 | "project": "./tsconfig.dev.json" 18 | }, 19 | "extends": [ 20 | "plugin:import/typescript" 21 | ], 22 | "settings": { 23 | "import/parsers": { 24 | "@typescript-eslint/parser": [ 25 | ".ts", 26 | ".tsx" 27 | ] 28 | }, 29 | "import/resolver": { 30 | "node": {}, 31 | "typescript": { 32 | "project": "./tsconfig.dev.json", 33 | "alwaysTryTypes": true 34 | } 35 | } 36 | }, 37 | "ignorePatterns": [ 38 | "*.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage", 43 | "!.projenrc.ts", 44 | "!projenrc/**/*.ts" 45 | ], 46 | "rules": { 47 | "@stylistic/indent": [ 48 | "error", 49 | 2 50 | ], 51 | "@stylistic/quotes": [ 52 | "error", 53 | "single", 54 | { 55 | "avoidEscape": true 56 | } 57 | ], 58 | "@stylistic/comma-dangle": [ 59 | "error", 60 | "always-multiline" 61 | ], 62 | "@stylistic/comma-spacing": [ 63 | "error", 64 | { 65 | "before": false, 66 | "after": true 67 | } 68 | ], 69 | "@stylistic/no-multi-spaces": [ 70 | "error", 71 | { 72 | "ignoreEOLComments": false 73 | } 74 | ], 75 | "@stylistic/array-bracket-spacing": [ 76 | "error", 77 | "never" 78 | ], 79 | "@stylistic/array-bracket-newline": [ 80 | "error", 81 | "consistent" 82 | ], 83 | "@stylistic/object-curly-spacing": [ 84 | "error", 85 | "always" 86 | ], 87 | "@stylistic/object-curly-newline": [ 88 | "error", 89 | { 90 | "multiline": true, 91 | "consistent": true 92 | } 93 | ], 94 | "@stylistic/object-property-newline": [ 95 | "error", 96 | { 97 | "allowAllPropertiesOnSameLine": true 98 | } 99 | ], 100 | "@stylistic/keyword-spacing": [ 101 | "error" 102 | ], 103 | "@stylistic/brace-style": [ 104 | "error", 105 | "1tbs", 106 | { 107 | "allowSingleLine": true 108 | } 109 | ], 110 | "@stylistic/space-before-blocks": [ 111 | "error" 112 | ], 113 | "@stylistic/member-delimiter-style": [ 114 | "error" 115 | ], 116 | "@stylistic/semi": [ 117 | "error", 118 | "always" 119 | ], 120 | "@stylistic/max-len": [ 121 | "error", 122 | { 123 | "code": 150, 124 | "ignoreUrls": true, 125 | "ignoreStrings": true, 126 | "ignoreTemplateLiterals": true, 127 | "ignoreComments": true, 128 | "ignoreRegExpLiterals": true 129 | } 130 | ], 131 | "@stylistic/quote-props": [ 132 | "error", 133 | "consistent-as-needed" 134 | ], 135 | "@stylistic/key-spacing": [ 136 | "error" 137 | ], 138 | "@stylistic/no-multiple-empty-lines": [ 139 | "error" 140 | ], 141 | "@stylistic/no-trailing-spaces": [ 142 | "error" 143 | ], 144 | "curly": [ 145 | "error", 146 | "multi-line", 147 | "consistent" 148 | ], 149 | "@typescript-eslint/no-require-imports": "error", 150 | "import/no-extraneous-dependencies": [ 151 | "error", 152 | { 153 | "devDependencies": [ 154 | "**/test/**", 155 | "**/build-tools/**", 156 | ".projenrc.ts", 157 | "projenrc/**/*.ts" 158 | ], 159 | "optionalDependencies": false, 160 | "peerDependencies": true 161 | } 162 | ], 163 | "import/no-unresolved": [ 164 | "error" 165 | ], 166 | "import/order": [ 167 | "warn", 168 | { 169 | "groups": [ 170 | "builtin", 171 | "external" 172 | ], 173 | "alphabetize": { 174 | "order": "asc", 175 | "caseInsensitive": true 176 | } 177 | } 178 | ], 179 | "import/no-duplicates": [ 180 | "error" 181 | ], 182 | "no-shadow": [ 183 | "off" 184 | ], 185 | "@typescript-eslint/no-shadow": "error", 186 | "@typescript-eslint/no-floating-promises": "error", 187 | "no-return-await": [ 188 | "off" 189 | ], 190 | "@typescript-eslint/return-await": "error", 191 | "dot-notation": [ 192 | "error" 193 | ], 194 | "no-bitwise": [ 195 | "error" 196 | ], 197 | "@typescript-eslint/member-ordering": [ 198 | "error", 199 | { 200 | "default": [ 201 | "public-static-field", 202 | "public-static-method", 203 | "protected-static-field", 204 | "protected-static-method", 205 | "private-static-field", 206 | "private-static-method", 207 | "field", 208 | "constructor", 209 | "method" 210 | ] 211 | } 212 | ] 213 | }, 214 | "overrides": [ 215 | { 216 | "files": [ 217 | ".projenrc.ts" 218 | ], 219 | "rules": { 220 | "@typescript-eslint/no-require-imports": "off", 221 | "import/no-extraneous-dependencies": "off" 222 | } 223 | } 224 | ] 225 | } 226 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.eslintrc.json linguist-generated 6 | /.gitattributes linguist-generated 7 | /.github/pull_request_template.md linguist-generated 8 | /.github/workflows/auto-approve.yml linguist-generated 9 | /.github/workflows/auto-queue.yml linguist-generated 10 | /.github/workflows/build.yml linguist-generated 11 | /.github/workflows/pull-request-lint.yml linguist-generated 12 | /.github/workflows/release.yml linguist-generated 13 | /.github/workflows/upgrade-cdklabs-projen-project-types-main.yml linguist-generated 14 | /.github/workflows/upgrade-dev-deps-main.yml linguist-generated 15 | /.github/workflows/upgrade-main.yml linguist-generated 16 | /.gitignore linguist-generated 17 | /.npmignore linguist-generated 18 | /.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 | /LICENSE linguist-generated 24 | /package.json linguist-generated 25 | /tsconfig.dev.json linguist-generated 26 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: auto-approve 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | approve: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && (github.event.pull_request.user.login == 'cdklabs-automation' || github.event.pull_request.user.login == 'dependabot[bot]') 18 | steps: 19 | - uses: hmarr/auto-approve-action@v2.2.1 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/auto-queue.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: auto-queue 4 | on: 5 | pull_request_target: 6 | types: 7 | - opened 8 | - reopened 9 | - ready_for_review 10 | jobs: 11 | enableAutoQueue: 12 | name: "Set AutoQueue on PR #${{ github.event.number }}" 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pull-requests: write 16 | contents: write 17 | steps: 18 | - uses: peter-evans/enable-pull-request-automerge@v3 19 | with: 20 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 21 | pull-request-number: ${{ github.event.number }} 22 | merge-method: squash 23 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | merge_group: {} 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | outputs: 14 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 15 | env: 16 | CI: "true" 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | ref: ${{ github.event.pull_request.head.ref }} 22 | repository: ${{ github.event.pull_request.head.repo.full_name }} 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: lts/* 27 | - name: Install dependencies 28 | run: yarn install --check-files 29 | - name: build 30 | run: npx projen build 31 | - name: Find mutations 32 | id: self_mutation 33 | run: |- 34 | git add . 35 | git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 36 | working-directory: ./ 37 | - name: Upload patch 38 | if: steps.self_mutation.outputs.self_mutation_happened 39 | uses: actions/upload-artifact@v4.4.0 40 | with: 41 | name: repo.patch 42 | path: repo.patch 43 | overwrite: true 44 | - name: Fail build on mutation 45 | if: steps.self_mutation.outputs.self_mutation_happened 46 | run: |- 47 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 48 | cat repo.patch 49 | exit 1 50 | - name: Backup artifact permissions 51 | run: cd dist && getfacl -R . > permissions-backup.acl 52 | continue-on-error: true 53 | - name: Upload artifact 54 | uses: actions/upload-artifact@v4.4.0 55 | with: 56 | name: build-artifact 57 | path: dist 58 | overwrite: true 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@v4 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@v4 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 | env: 85 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 86 | run: |- 87 | git add . 88 | git commit -s -m "chore: self mutation" 89 | git push origin HEAD:$PULL_REQUEST_REF 90 | package-js: 91 | needs: build 92 | runs-on: ubuntu-latest 93 | permissions: 94 | contents: read 95 | if: ${{ !needs.build.outputs.self_mutation_happened }} 96 | steps: 97 | - uses: actions/setup-node@v4 98 | with: 99 | node-version: lts/* 100 | - name: Download build artifacts 101 | uses: actions/download-artifact@v4 102 | with: 103 | name: build-artifact 104 | path: dist 105 | - name: Restore build artifact permissions 106 | run: cd dist && setfacl --restore=permissions-backup.acl 107 | continue-on-error: true 108 | - name: Checkout 109 | uses: actions/checkout@v4 110 | with: 111 | ref: ${{ github.event.pull_request.head.ref }} 112 | repository: ${{ github.event.pull_request.head.repo.full_name }} 113 | path: .repo 114 | - name: Install Dependencies 115 | run: cd .repo && yarn install --check-files --frozen-lockfile 116 | - name: Extract build artifact 117 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 118 | - name: Move build artifact out of the way 119 | run: mv dist dist.old 120 | - name: Create js artifact 121 | run: cd .repo && npx projen package:js 122 | - name: Collect js artifact 123 | run: mv .repo/dist dist 124 | package-java: 125 | needs: build 126 | runs-on: ubuntu-latest 127 | permissions: 128 | contents: read 129 | if: ${{ !needs.build.outputs.self_mutation_happened }} 130 | steps: 131 | - uses: actions/setup-java@v4 132 | with: 133 | distribution: corretto 134 | java-version: "11" 135 | - uses: actions/setup-node@v4 136 | with: 137 | node-version: lts/* 138 | - name: Download build artifacts 139 | uses: actions/download-artifact@v4 140 | with: 141 | name: build-artifact 142 | path: dist 143 | - name: Restore build artifact permissions 144 | run: cd dist && setfacl --restore=permissions-backup.acl 145 | continue-on-error: true 146 | - name: Checkout 147 | uses: actions/checkout@v4 148 | with: 149 | ref: ${{ github.event.pull_request.head.ref }} 150 | repository: ${{ github.event.pull_request.head.repo.full_name }} 151 | path: .repo 152 | - name: Install Dependencies 153 | run: cd .repo && yarn install --check-files --frozen-lockfile 154 | - name: Extract build artifact 155 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 156 | - name: Move build artifact out of the way 157 | run: mv dist dist.old 158 | - name: Create java artifact 159 | run: cd .repo && npx projen package:java 160 | - name: Collect java artifact 161 | run: mv .repo/dist dist 162 | package-python: 163 | needs: build 164 | runs-on: ubuntu-latest 165 | permissions: 166 | contents: read 167 | if: ${{ !needs.build.outputs.self_mutation_happened }} 168 | steps: 169 | - uses: actions/setup-node@v4 170 | with: 171 | node-version: lts/* 172 | - uses: actions/setup-python@v5 173 | with: 174 | python-version: 3.x 175 | - name: Download build artifacts 176 | uses: actions/download-artifact@v4 177 | with: 178 | name: build-artifact 179 | path: dist 180 | - name: Restore build artifact permissions 181 | run: cd dist && setfacl --restore=permissions-backup.acl 182 | continue-on-error: true 183 | - name: Checkout 184 | uses: actions/checkout@v4 185 | with: 186 | ref: ${{ github.event.pull_request.head.ref }} 187 | repository: ${{ github.event.pull_request.head.repo.full_name }} 188 | path: .repo 189 | - name: Install Dependencies 190 | run: cd .repo && yarn install --check-files --frozen-lockfile 191 | - name: Extract build artifact 192 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 193 | - name: Move build artifact out of the way 194 | run: mv dist dist.old 195 | - name: Create python artifact 196 | run: cd .repo && npx projen package:python 197 | - name: Collect python artifact 198 | run: mv .repo/dist dist 199 | package-dotnet: 200 | needs: build 201 | runs-on: ubuntu-latest 202 | permissions: 203 | contents: read 204 | if: ${{ !needs.build.outputs.self_mutation_happened }} 205 | steps: 206 | - uses: actions/setup-node@v4 207 | with: 208 | node-version: lts/* 209 | - uses: actions/setup-dotnet@v4 210 | with: 211 | dotnet-version: 6.x 212 | - name: Download build artifacts 213 | uses: actions/download-artifact@v4 214 | with: 215 | name: build-artifact 216 | path: dist 217 | - name: Restore build artifact permissions 218 | run: cd dist && setfacl --restore=permissions-backup.acl 219 | continue-on-error: true 220 | - name: Checkout 221 | uses: actions/checkout@v4 222 | with: 223 | ref: ${{ github.event.pull_request.head.ref }} 224 | repository: ${{ github.event.pull_request.head.repo.full_name }} 225 | path: .repo 226 | - name: Install Dependencies 227 | run: cd .repo && yarn install --check-files --frozen-lockfile 228 | - name: Extract build artifact 229 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 230 | - name: Move build artifact out of the way 231 | run: mv dist dist.old 232 | - name: Create dotnet artifact 233 | run: cd .repo && npx projen package:dotnet 234 | - name: Collect dotnet artifact 235 | run: mv .repo/dist dist 236 | package-go: 237 | needs: build 238 | runs-on: ubuntu-latest 239 | permissions: 240 | contents: read 241 | if: ${{ !needs.build.outputs.self_mutation_happened }} 242 | steps: 243 | - uses: actions/setup-node@v4 244 | with: 245 | node-version: lts/* 246 | - uses: actions/setup-go@v5 247 | with: 248 | go-version: ^1.18.0 249 | - name: Download build artifacts 250 | uses: actions/download-artifact@v4 251 | with: 252 | name: build-artifact 253 | path: dist 254 | - name: Restore build artifact permissions 255 | run: cd dist && setfacl --restore=permissions-backup.acl 256 | continue-on-error: true 257 | - name: Checkout 258 | uses: actions/checkout@v4 259 | with: 260 | ref: ${{ github.event.pull_request.head.ref }} 261 | repository: ${{ github.event.pull_request.head.repo.full_name }} 262 | path: .repo 263 | - name: Install Dependencies 264 | run: cd .repo && yarn install --check-files --frozen-lockfile 265 | - name: Extract build artifact 266 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 267 | - name: Move build artifact out of the way 268 | run: mv dist dist.old 269 | - name: Create go artifact 270 | run: cd .repo && npx projen package:go 271 | - name: Collect go artifact 272 | run: mv .repo/dist dist 273 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | merge_group: {} 14 | jobs: 15 | validate: 16 | name: Validate PR title 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: write 20 | if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') 21 | steps: 22 | - uses: amannn/action-semantic-pull-request@v5.4.0 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | types: |- 27 | feat 28 | fix 29 | chore 30 | requireScope: false 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: false 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | outputs: 18 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 19 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 20 | env: 21 | CI: "true" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Set git identity 28 | run: |- 29 | git config user.name "github-actions" 30 | git config user.email "github-actions@github.com" 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: lts/* 35 | - name: Install dependencies 36 | run: yarn install --check-files --frozen-lockfile 37 | - name: release 38 | run: npx projen release 39 | - name: Check if version has already been tagged 40 | id: check_tag_exists 41 | run: |- 42 | TAG=$(cat dist/releasetag.txt) 43 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 44 | cat $GITHUB_OUTPUT 45 | - name: Check for new commits 46 | id: git_remote 47 | run: |- 48 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 49 | cat $GITHUB_OUTPUT 50 | - name: Backup artifact permissions 51 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 52 | run: cd dist && getfacl -R . > permissions-backup.acl 53 | continue-on-error: true 54 | - name: Upload artifact 55 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 56 | uses: actions/upload-artifact@v4.4.0 57 | with: 58 | name: build-artifact 59 | path: dist 60 | overwrite: true 61 | release_github: 62 | name: Publish to GitHub Releases 63 | needs: 64 | - release 65 | - release_npm 66 | - release_maven 67 | - release_pypi 68 | - release_nuget 69 | - release_golang 70 | runs-on: ubuntu-latest 71 | permissions: 72 | contents: write 73 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 74 | steps: 75 | - uses: actions/setup-node@v4 76 | with: 77 | node-version: lts/* 78 | - name: Download build artifacts 79 | uses: actions/download-artifact@v4 80 | with: 81 | name: build-artifact 82 | path: dist 83 | - name: Restore build artifact permissions 84 | run: cd dist && setfacl --restore=permissions-backup.acl 85 | continue-on-error: true 86 | - name: Release 87 | env: 88 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_SHA 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi 90 | release_npm: 91 | name: Publish to npm 92 | needs: release 93 | runs-on: ubuntu-latest 94 | permissions: 95 | id-token: write 96 | contents: read 97 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 98 | steps: 99 | - uses: actions/setup-node@v4 100 | with: 101 | node-version: lts/* 102 | - name: Download build artifacts 103 | uses: actions/download-artifact@v4 104 | with: 105 | name: build-artifact 106 | path: dist 107 | - name: Restore build artifact permissions 108 | run: cd dist && setfacl --restore=permissions-backup.acl 109 | continue-on-error: true 110 | - name: Checkout 111 | uses: actions/checkout@v4 112 | with: 113 | path: .repo 114 | - name: Install Dependencies 115 | run: cd .repo && yarn install --check-files --frozen-lockfile 116 | - name: Extract build artifact 117 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 118 | - name: Move build artifact out of the way 119 | run: mv dist dist.old 120 | - name: Create js artifact 121 | run: cd .repo && npx projen package:js 122 | - name: Collect js artifact 123 | run: mv .repo/dist dist 124 | - name: Release 125 | env: 126 | NPM_DIST_TAG: latest 127 | NPM_REGISTRY: registry.npmjs.org 128 | NPM_CONFIG_PROVENANCE: "true" 129 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 130 | run: npx -p publib@latest publib-npm 131 | release_maven: 132 | name: Publish to Maven Central 133 | needs: release 134 | runs-on: ubuntu-latest 135 | permissions: 136 | contents: read 137 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 138 | steps: 139 | - uses: actions/setup-java@v4 140 | with: 141 | distribution: corretto 142 | java-version: "11" 143 | - uses: actions/setup-node@v4 144 | with: 145 | node-version: lts/* 146 | - name: Download build artifacts 147 | uses: actions/download-artifact@v4 148 | with: 149 | name: build-artifact 150 | path: dist 151 | - name: Restore build artifact permissions 152 | run: cd dist && setfacl --restore=permissions-backup.acl 153 | continue-on-error: true 154 | - name: Checkout 155 | uses: actions/checkout@v4 156 | with: 157 | path: .repo 158 | - name: Install Dependencies 159 | run: cd .repo && yarn install --check-files --frozen-lockfile 160 | - name: Extract build artifact 161 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 162 | - name: Move build artifact out of the way 163 | run: mv dist dist.old 164 | - name: Create java artifact 165 | run: cd .repo && npx projen package:java 166 | - name: Collect java artifact 167 | run: mv .repo/dist dist 168 | - name: Release 169 | env: 170 | MAVEN_SERVER_ID: central-ossrh 171 | MAVEN_GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 172 | MAVEN_GPG_PRIVATE_KEY_PASSPHRASE: ${{ secrets.MAVEN_GPG_PRIVATE_KEY_PASSPHRASE }} 173 | MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} 174 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 175 | MAVEN_STAGING_PROFILE_ID: ${{ secrets.MAVEN_STAGING_PROFILE_ID }} 176 | run: npx -p publib@latest publib-maven 177 | release_pypi: 178 | name: Publish to PyPI 179 | needs: release 180 | runs-on: ubuntu-latest 181 | permissions: 182 | contents: read 183 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 184 | steps: 185 | - uses: actions/setup-node@v4 186 | with: 187 | node-version: lts/* 188 | - uses: actions/setup-python@v5 189 | with: 190 | python-version: 3.x 191 | - name: Download build artifacts 192 | uses: actions/download-artifact@v4 193 | with: 194 | name: build-artifact 195 | path: dist 196 | - name: Restore build artifact permissions 197 | run: cd dist && setfacl --restore=permissions-backup.acl 198 | continue-on-error: true 199 | - name: Checkout 200 | uses: actions/checkout@v4 201 | with: 202 | path: .repo 203 | - name: Install Dependencies 204 | run: cd .repo && yarn install --check-files --frozen-lockfile 205 | - name: Extract build artifact 206 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 207 | - name: Move build artifact out of the way 208 | run: mv dist dist.old 209 | - name: Create python artifact 210 | run: cd .repo && npx projen package:python 211 | - name: Collect python artifact 212 | run: mv .repo/dist dist 213 | - name: Release 214 | env: 215 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 216 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 217 | run: npx -p publib@latest publib-pypi 218 | release_nuget: 219 | name: Publish to NuGet Gallery 220 | needs: release 221 | runs-on: ubuntu-latest 222 | permissions: 223 | contents: read 224 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 225 | steps: 226 | - uses: actions/setup-node@v4 227 | with: 228 | node-version: lts/* 229 | - uses: actions/setup-dotnet@v4 230 | with: 231 | dotnet-version: 6.x 232 | - name: Download build artifacts 233 | uses: actions/download-artifact@v4 234 | with: 235 | name: build-artifact 236 | path: dist 237 | - name: Restore build artifact permissions 238 | run: cd dist && setfacl --restore=permissions-backup.acl 239 | continue-on-error: true 240 | - name: Checkout 241 | uses: actions/checkout@v4 242 | with: 243 | path: .repo 244 | - name: Install Dependencies 245 | run: cd .repo && yarn install --check-files --frozen-lockfile 246 | - name: Extract build artifact 247 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 248 | - name: Move build artifact out of the way 249 | run: mv dist dist.old 250 | - name: Create dotnet artifact 251 | run: cd .repo && npx projen package:dotnet 252 | - name: Collect dotnet artifact 253 | run: mv .repo/dist dist 254 | - name: Release 255 | env: 256 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 257 | run: npx -p publib@latest publib-nuget 258 | release_golang: 259 | name: Publish to GitHub Go Module Repository 260 | needs: release 261 | runs-on: ubuntu-latest 262 | permissions: 263 | contents: read 264 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 265 | steps: 266 | - uses: actions/setup-node@v4 267 | with: 268 | node-version: lts/* 269 | - uses: actions/setup-go@v5 270 | with: 271 | go-version: ^1.18.0 272 | - name: Download build artifacts 273 | uses: actions/download-artifact@v4 274 | with: 275 | name: build-artifact 276 | path: dist 277 | - name: Restore build artifact permissions 278 | run: cd dist && setfacl --restore=permissions-backup.acl 279 | continue-on-error: true 280 | - name: Checkout 281 | uses: actions/checkout@v4 282 | with: 283 | path: .repo 284 | - name: Install Dependencies 285 | run: cd .repo && yarn install --check-files --frozen-lockfile 286 | - name: Extract build artifact 287 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 288 | - name: Move build artifact out of the way 289 | run: mv dist dist.old 290 | - name: Create go artifact 291 | run: cd .repo && npx projen package:go 292 | - name: Collect go artifact 293 | run: mv .repo/dist dist 294 | - name: Release 295 | env: 296 | GIT_USER_NAME: github-actions 297 | GIT_USER_EMAIL: github-actions@github.com 298 | GITHUB_TOKEN: ${{ secrets.GO_GITHUB_TOKEN }} 299 | run: npx -p publib@latest publib-golang 300 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-cdklabs-projen-project-types-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-cdklabs-projen-project-types-main 4 | on: 5 | workflow_dispatch: {} 6 | jobs: 7 | upgrade: 8 | name: Upgrade 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: read 12 | outputs: 13 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | ref: main 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v4 21 | - name: Install dependencies 22 | run: yarn install --check-files --frozen-lockfile 23 | - name: Upgrade dependencies 24 | run: npx projen upgrade-cdklabs-projen-project-types 25 | - name: Find mutations 26 | id: create_patch 27 | run: |- 28 | git add . 29 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 30 | working-directory: ./ 31 | - name: Upload patch 32 | if: steps.create_patch.outputs.patch_created 33 | uses: actions/upload-artifact@v4.4.0 34 | with: 35 | name: repo.patch 36 | path: repo.patch 37 | overwrite: true 38 | pr: 39 | name: Create Pull Request 40 | needs: upgrade 41 | runs-on: ubuntu-latest 42 | permissions: 43 | contents: read 44 | if: ${{ needs.upgrade.outputs.patch_created }} 45 | steps: 46 | - name: Checkout 47 | uses: actions/checkout@v4 48 | with: 49 | ref: main 50 | - name: Download patch 51 | uses: actions/download-artifact@v4 52 | with: 53 | name: repo.patch 54 | path: ${{ runner.temp }} 55 | - name: Apply patch 56 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 57 | - name: Set git identity 58 | run: |- 59 | git config user.name "github-actions" 60 | git config user.email "github-actions@github.com" 61 | - name: Create Pull Request 62 | id: create-pr 63 | uses: peter-evans/create-pull-request@v6 64 | with: 65 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 66 | commit-message: |- 67 | chore(deps): upgrade cdklabs-projen-project-types 68 | 69 | Upgrades project dependencies. See details in [workflow run]. 70 | 71 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 72 | 73 | ------ 74 | 75 | *Automatically created by projen via the "upgrade-cdklabs-projen-project-types-main" workflow* 76 | branch: github-actions/upgrade-cdklabs-projen-project-types-main 77 | title: "chore(deps): upgrade cdklabs-projen-project-types" 78 | labels: auto-approve 79 | body: |- 80 | Upgrades project dependencies. See details in [workflow run]. 81 | 82 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 83 | 84 | ------ 85 | 86 | *Automatically created by projen via the "upgrade-cdklabs-projen-project-types-main" workflow* 87 | author: github-actions 88 | committer: github-actions 89 | signoff: true 90 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-dev-deps-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-dev-deps-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 18 * * 1 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade-dev-deps 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | chore(deps): upgrade dev dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-dev-deps-main" workflow* 80 | branch: github-actions/upgrade-dev-deps-main 81 | title: "chore(deps): upgrade dev dependencies" 82 | labels: auto-approve 83 | body: |- 84 | Upgrades project dependencies. See details in [workflow run]. 85 | 86 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 87 | 88 | ------ 89 | 90 | *Automatically created by projen via the "upgrade-dev-deps-main" workflow* 91 | author: github-actions 92 | committer: github-actions 93 | signoff: true 94 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 18 * * 1 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: lts/* 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | fix(deps): upgrade dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-main" workflow* 80 | branch: github-actions/upgrade-main 81 | title: "fix(deps): upgrade dependencies" 82 | labels: auto-approve 83 | body: |- 84 | Upgrades project dependencies. See details in [workflow run]. 85 | 86 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 87 | 88 | ------ 89 | 90 | *Automatically created by projen via the "upgrade-main" workflow* 91 | author: github-actions 92 | committer: github-actions 93 | signoff: true 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/.github/workflows/auto-approve.yml 8 | !/package.json 9 | !/LICENSE 10 | !/.npmignore 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | lib-cov 23 | coverage 24 | *.lcov 25 | .nyc_output 26 | build/Release 27 | node_modules/ 28 | jspm_packages/ 29 | *.tsbuildinfo 30 | .eslintcache 31 | *.tgz 32 | .yarn-integrity 33 | .cache 34 | cdk.out 35 | lambda-bin/bootstrap 36 | /test-reports/ 37 | junit.xml 38 | /coverage/ 39 | !/.github/workflows/build.yml 40 | /dist/changelog.md 41 | /dist/version.txt 42 | !/.github/workflows/release.yml 43 | !/.github/pull_request_template.md 44 | !/test/ 45 | !/tsconfig.dev.json 46 | !/src/ 47 | /lib 48 | /dist/ 49 | !/.eslintrc.json 50 | .jsii 51 | tsconfig.json 52 | !/API.md 53 | .jsii.tabl.json 54 | !/rosetta/default.ts-fixture 55 | !/.github/workflows/auto-queue.yml 56 | !/.github/workflows/upgrade-cdklabs-projen-project-types-main.yml 57 | !/.github/workflows/upgrade-main.yml 58 | !/.github/workflows/upgrade-dev-deps-main.yml 59 | !/.projenrc.ts 60 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | cdk.out 3 | build-lambda.sh 4 | lambda-src 5 | /.projen/ 6 | /test-reports/ 7 | junit.xml 8 | /coverage/ 9 | permissions-backup.acl 10 | /dist/changelog.md 11 | /dist/version.txt 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 | /.gitattributes 28 | /.projenrc.ts 29 | /projenrc 30 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@stylistic/eslint-plugin", 5 | "version": "^2", 6 | "type": "build" 7 | }, 8 | { 9 | "name": "@types/jest", 10 | "type": "build" 11 | }, 12 | { 13 | "name": "@types/node", 14 | "version": "^18", 15 | "type": "build" 16 | }, 17 | { 18 | "name": "@typescript-eslint/eslint-plugin", 19 | "version": "^8", 20 | "type": "build" 21 | }, 22 | { 23 | "name": "@typescript-eslint/parser", 24 | "version": "^8", 25 | "type": "build" 26 | }, 27 | { 28 | "name": "cdklabs-projen-project-types", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "commit-and-tag-version", 33 | "version": "^12", 34 | "type": "build" 35 | }, 36 | { 37 | "name": "eslint-import-resolver-typescript", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "eslint-plugin-import", 42 | "type": "build" 43 | }, 44 | { 45 | "name": "eslint", 46 | "version": "^9", 47 | "type": "build" 48 | }, 49 | { 50 | "name": "jest", 51 | "type": "build" 52 | }, 53 | { 54 | "name": "jest-junit", 55 | "version": "^16", 56 | "type": "build" 57 | }, 58 | { 59 | "name": "jsii-diff", 60 | "type": "build" 61 | }, 62 | { 63 | "name": "jsii-docgen", 64 | "version": "^10.5.0", 65 | "type": "build" 66 | }, 67 | { 68 | "name": "jsii-pacmak", 69 | "type": "build" 70 | }, 71 | { 72 | "name": "jsii-rosetta", 73 | "type": "build" 74 | }, 75 | { 76 | "name": "jsii", 77 | "version": "5.7.x", 78 | "type": "build" 79 | }, 80 | { 81 | "name": "projen", 82 | "type": "build" 83 | }, 84 | { 85 | "name": "ts-jest", 86 | "type": "build" 87 | }, 88 | { 89 | "name": "ts-node", 90 | "type": "build" 91 | }, 92 | { 93 | "name": "typescript", 94 | "version": "5.7.x", 95 | "type": "build" 96 | }, 97 | { 98 | "name": "@aws-cdk/integ-runner", 99 | "version": "latest", 100 | "type": "devenv" 101 | }, 102 | { 103 | "name": "@aws-cdk/integ-tests-alpha", 104 | "version": "latest", 105 | "type": "devenv" 106 | }, 107 | { 108 | "name": "aws-cdk-lib", 109 | "version": "^2.80.0", 110 | "type": "peer" 111 | }, 112 | { 113 | "name": "constructs", 114 | "version": "^10.0.5", 115 | "type": "peer" 116 | } 117 | ], 118 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 119 | } 120 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/auto-approve.yml", 7 | ".github/workflows/auto-queue.yml", 8 | ".github/workflows/build.yml", 9 | ".github/workflows/pull-request-lint.yml", 10 | ".github/workflows/release.yml", 11 | ".github/workflows/upgrade-cdklabs-projen-project-types-main.yml", 12 | ".github/workflows/upgrade-dev-deps-main.yml", 13 | ".github/workflows/upgrade-main.yml", 14 | ".gitignore", 15 | ".projen/deps.json", 16 | ".projen/files.json", 17 | ".projen/tasks.json", 18 | "LICENSE", 19 | "tsconfig.dev.json" 20 | ], 21 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 22 | } 23 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "", 36 | "BUMP_PACKAGE": "commit-and-tag-version@^12", 37 | "RELEASABLE_COMMITS": "git log --no-merges --oneline $LATEST_TAG..HEAD -E --grep \"^(feat|fix){1}(\\([^()[:space:]]+\\))?(!)?:[[:blank:]]+.+\"" 38 | }, 39 | "steps": [ 40 | { 41 | "builtin": "release/bump-version" 42 | } 43 | ], 44 | "condition": "git log --oneline -1 | grep -qv \"chore(release):\"" 45 | }, 46 | "clobber": { 47 | "name": "clobber", 48 | "description": "hard resets to HEAD of origin and cleans the local repo", 49 | "env": { 50 | "BRANCH": "$(git branch --show-current)" 51 | }, 52 | "steps": [ 53 | { 54 | "exec": "git checkout -b scratch", 55 | "name": "save current HEAD in \"scratch\" branch" 56 | }, 57 | { 58 | "exec": "git checkout $BRANCH" 59 | }, 60 | { 61 | "exec": "git fetch origin", 62 | "name": "fetch latest changes from origin" 63 | }, 64 | { 65 | "exec": "git reset --hard origin/$BRANCH", 66 | "name": "hard reset to origin commit" 67 | }, 68 | { 69 | "exec": "git clean -fdx", 70 | "name": "clean all untracked files" 71 | }, 72 | { 73 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 74 | } 75 | ], 76 | "condition": "git diff --exit-code > /dev/null" 77 | }, 78 | "compat": { 79 | "name": "compat", 80 | "description": "Perform API compatibility check against latest version", 81 | "steps": [ 82 | { 83 | "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)" 84 | } 85 | ] 86 | }, 87 | "compile": { 88 | "name": "compile", 89 | "description": "Only compile", 90 | "steps": [ 91 | { 92 | "exec": "jsii --silence-warnings=reserved-word" 93 | } 94 | ] 95 | }, 96 | "default": { 97 | "name": "default", 98 | "description": "Synthesize project files", 99 | "steps": [ 100 | { 101 | "exec": "ts-node --project tsconfig.dev.json .projenrc.ts" 102 | } 103 | ] 104 | }, 105 | "docgen": { 106 | "name": "docgen", 107 | "description": "Generate API.md from .jsii manifest", 108 | "steps": [ 109 | { 110 | "exec": "jsii-docgen -o API.md" 111 | } 112 | ] 113 | }, 114 | "eject": { 115 | "name": "eject", 116 | "description": "Remove projen from the project", 117 | "env": { 118 | "PROJEN_EJECTING": "true" 119 | }, 120 | "steps": [ 121 | { 122 | "spawn": "default" 123 | } 124 | ] 125 | }, 126 | "eslint": { 127 | "name": "eslint", 128 | "description": "Runs eslint against the codebase", 129 | "env": { 130 | "ESLINT_USE_FLAT_CONFIG": "false" 131 | }, 132 | "steps": [ 133 | { 134 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern $@ src test build-tools projenrc .projenrc.ts", 135 | "receiveArgs": true 136 | } 137 | ] 138 | }, 139 | "install": { 140 | "name": "install", 141 | "description": "Install project dependencies and update lockfile (non-frozen)", 142 | "steps": [ 143 | { 144 | "exec": "yarn install --check-files" 145 | } 146 | ] 147 | }, 148 | "install:ci": { 149 | "name": "install:ci", 150 | "description": "Install project dependencies using frozen lockfile", 151 | "steps": [ 152 | { 153 | "exec": "yarn install --check-files --frozen-lockfile" 154 | } 155 | ] 156 | }, 157 | "integ": { 158 | "name": "integ", 159 | "description": "Run integration snapshot tests", 160 | "steps": [ 161 | { 162 | "exec": "yarn integ-runner --language typescript", 163 | "receiveArgs": true 164 | } 165 | ] 166 | }, 167 | "integ:update": { 168 | "name": "integ:update", 169 | "description": "Run and update integration snapshot tests", 170 | "steps": [ 171 | { 172 | "exec": "yarn integ-runner --language typescript --update-on-failed", 173 | "receiveArgs": true 174 | } 175 | ] 176 | }, 177 | "package": { 178 | "name": "package", 179 | "description": "Creates the distribution package", 180 | "steps": [ 181 | { 182 | "spawn": "package:js", 183 | "condition": "node -e \"if (!process.env.CI) process.exit(1)\"" 184 | }, 185 | { 186 | "spawn": "package-all", 187 | "condition": "node -e \"if (process.env.CI) process.exit(1)\"" 188 | } 189 | ] 190 | }, 191 | "package-all": { 192 | "name": "package-all", 193 | "description": "Packages artifacts for all target languages", 194 | "steps": [ 195 | { 196 | "spawn": "package:js" 197 | }, 198 | { 199 | "spawn": "package:java" 200 | }, 201 | { 202 | "spawn": "package:python" 203 | }, 204 | { 205 | "spawn": "package:dotnet" 206 | }, 207 | { 208 | "spawn": "package:go" 209 | } 210 | ] 211 | }, 212 | "package:dotnet": { 213 | "name": "package:dotnet", 214 | "description": "Create dotnet language bindings", 215 | "steps": [ 216 | { 217 | "exec": "jsii-pacmak -v --target dotnet" 218 | } 219 | ] 220 | }, 221 | "package:go": { 222 | "name": "package:go", 223 | "description": "Create go language bindings", 224 | "steps": [ 225 | { 226 | "exec": "jsii-pacmak -v --target go" 227 | } 228 | ] 229 | }, 230 | "package:java": { 231 | "name": "package:java", 232 | "description": "Create java language bindings", 233 | "steps": [ 234 | { 235 | "exec": "jsii-pacmak -v --target java" 236 | } 237 | ] 238 | }, 239 | "package:js": { 240 | "name": "package:js", 241 | "description": "Create js language bindings", 242 | "steps": [ 243 | { 244 | "exec": "jsii-pacmak -v --target js" 245 | } 246 | ] 247 | }, 248 | "package:python": { 249 | "name": "package:python", 250 | "description": "Create python language bindings", 251 | "steps": [ 252 | { 253 | "exec": "jsii-pacmak -v --target python" 254 | } 255 | ] 256 | }, 257 | "post-compile": { 258 | "name": "post-compile", 259 | "description": "Runs after successful compilation", 260 | "steps": [ 261 | { 262 | "spawn": "docgen" 263 | }, 264 | { 265 | "spawn": "rosetta:extract" 266 | } 267 | ] 268 | }, 269 | "post-upgrade": { 270 | "name": "post-upgrade", 271 | "description": "Runs after upgrading dependencies" 272 | }, 273 | "pre-compile": { 274 | "name": "pre-compile", 275 | "description": "Prepare the project for compilation", 276 | "steps": [ 277 | { 278 | "exec": "./build-lambda.sh" 279 | } 280 | ] 281 | }, 282 | "release": { 283 | "name": "release", 284 | "description": "Prepare a release from \"main\" branch", 285 | "env": { 286 | "RELEASE": "true", 287 | "MAJOR": "4" 288 | }, 289 | "steps": [ 290 | { 291 | "exec": "rm -fr dist" 292 | }, 293 | { 294 | "spawn": "bump" 295 | }, 296 | { 297 | "spawn": "build" 298 | }, 299 | { 300 | "spawn": "unbump" 301 | }, 302 | { 303 | "exec": "git diff --ignore-space-at-eol --exit-code" 304 | } 305 | ] 306 | }, 307 | "rosetta:extract": { 308 | "name": "rosetta:extract", 309 | "description": "Test rosetta extract", 310 | "steps": [ 311 | { 312 | "exec": "yarn --silent jsii-rosetta extract --strict" 313 | } 314 | ] 315 | }, 316 | "test": { 317 | "name": "test", 318 | "description": "Run tests", 319 | "steps": [ 320 | { 321 | "exec": "jest --passWithNoTests --updateSnapshot", 322 | "receiveArgs": true 323 | }, 324 | { 325 | "spawn": "eslint" 326 | }, 327 | { 328 | "spawn": "integ" 329 | } 330 | ] 331 | }, 332 | "test:watch": { 333 | "name": "test:watch", 334 | "description": "Run jest in watch mode", 335 | "steps": [ 336 | { 337 | "exec": "jest --watch" 338 | } 339 | ] 340 | }, 341 | "unbump": { 342 | "name": "unbump", 343 | "description": "Restores version to 0.0.0", 344 | "env": { 345 | "OUTFILE": "package.json", 346 | "CHANGELOG": "dist/changelog.md", 347 | "BUMPFILE": "dist/version.txt", 348 | "RELEASETAG": "dist/releasetag.txt", 349 | "RELEASE_TAG_PREFIX": "", 350 | "BUMP_PACKAGE": "commit-and-tag-version@^12", 351 | "RELEASABLE_COMMITS": "git log --no-merges --oneline $LATEST_TAG..HEAD -E --grep \"^(feat|fix){1}(\\([^()[:space:]]+\\))?(!)?:[[:blank:]]+.+\"" 352 | }, 353 | "steps": [ 354 | { 355 | "builtin": "release/reset-version" 356 | } 357 | ] 358 | }, 359 | "upgrade": { 360 | "name": "upgrade", 361 | "description": "upgrade dependencies", 362 | "env": { 363 | "CI": "0" 364 | }, 365 | "steps": [ 366 | { 367 | "exec": "echo No dependencies to upgrade." 368 | } 369 | ] 370 | }, 371 | "upgrade-cdklabs-projen-project-types": { 372 | "name": "upgrade-cdklabs-projen-project-types", 373 | "description": "upgrade cdklabs-projen-project-types", 374 | "env": { 375 | "CI": "0" 376 | }, 377 | "steps": [ 378 | { 379 | "exec": "npx npm-check-updates@16 --upgrade --target=latest --peer --no-deprecated --dep=dev,peer,prod,optional --filter=cdklabs-projen-project-types,projen" 380 | }, 381 | { 382 | "exec": "yarn install --check-files" 383 | }, 384 | { 385 | "exec": "yarn upgrade cdklabs-projen-project-types projen" 386 | }, 387 | { 388 | "exec": "npx projen" 389 | }, 390 | { 391 | "spawn": "post-upgrade" 392 | } 393 | ] 394 | }, 395 | "upgrade-dev-deps": { 396 | "name": "upgrade-dev-deps", 397 | "description": "upgrade dev dependencies", 398 | "env": { 399 | "CI": "0" 400 | }, 401 | "steps": [ 402 | { 403 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --dep=dev --filter=@types/jest,eslint-import-resolver-typescript,eslint-plugin-import,jest,jsii-diff,jsii-pacmak,jsii-rosetta,ts-jest,ts-node" 404 | }, 405 | { 406 | "exec": "yarn install --check-files" 407 | }, 408 | { 409 | "exec": "yarn upgrade @stylistic/eslint-plugin @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser commit-and-tag-version eslint-import-resolver-typescript eslint-plugin-import eslint jest jest-junit jsii-diff jsii-docgen jsii-pacmak jsii-rosetta jsii ts-jest ts-node typescript @aws-cdk/integ-runner @aws-cdk/integ-tests-alpha" 410 | }, 411 | { 412 | "exec": "npx projen" 413 | }, 414 | { 415 | "spawn": "post-upgrade" 416 | } 417 | ] 418 | }, 419 | "watch": { 420 | "name": "watch", 421 | "description": "Watch & compile in the background", 422 | "steps": [ 423 | { 424 | "exec": "jsii -w --silence-warnings=reserved-word" 425 | } 426 | ] 427 | } 428 | }, 429 | "env": { 430 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 431 | }, 432 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 433 | } 434 | -------------------------------------------------------------------------------- /.projenrc.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { CdklabsConstructLibrary } from 'cdklabs-projen-project-types'; 5 | 6 | const project = new CdklabsConstructLibrary({ 7 | name: 'cdk-ecr-deployment', 8 | stability: 'stable', 9 | private: false, 10 | projenrcTs: true, 11 | 12 | description: 'CDK construct to deploy docker image to Amazon ECR', 13 | repositoryUrl: 'https://github.com/cdklabs/cdk-ecr-deployment', 14 | author: 'Amazon Web Services', 15 | authorAddress: 'https://aws.amazon.com', 16 | 17 | defaultReleaseBranch: 'main', 18 | majorVersion: 4, 19 | publishToPypi: { 20 | distName: 'cdk-ecr-deployment', 21 | module: 'cdk_ecr_deployment', 22 | }, 23 | 24 | jsiiVersion: '5.7.x', 25 | typescriptVersion: '5.7.x', 26 | cdkVersion: '2.80.0', 27 | cdkVersionPinning: false, 28 | bundledDeps: [], 29 | deps: [], 30 | 31 | gitignore: [ 32 | 'cdk.out', 33 | 'lambda-bin/bootstrap', 34 | ], 35 | npmignore: [ 36 | 'cdk.out', 37 | 'build-lambda.sh', 38 | 'lambda-src', 39 | ], 40 | 41 | enablePRAutoMerge: true, 42 | setNodeEngineVersion: false, 43 | }); 44 | 45 | project.package.addField('jsiiRosetta', { 46 | exampleDependencies: { 47 | '@types/node': '^18', 48 | }, 49 | }); 50 | 51 | project.preCompileTask.exec('./build-lambda.sh'); 52 | 53 | project.synth(); 54 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## Constructs 4 | 5 | ### ECRDeployment 6 | 7 | #### Initializers 8 | 9 | ```typescript 10 | import { ECRDeployment } from 'cdk-ecr-deployment' 11 | 12 | new ECRDeployment(scope: Construct, id: string, props: ECRDeploymentProps) 13 | ``` 14 | 15 | | **Name** | **Type** | **Description** | 16 | | --- | --- | --- | 17 | | scope | constructs.Construct | *No description.* | 18 | | id | string | *No description.* | 19 | | props | ECRDeploymentProps | *No description.* | 20 | 21 | --- 22 | 23 | ##### `scope`Required 24 | 25 | - *Type:* constructs.Construct 26 | 27 | --- 28 | 29 | ##### `id`Required 30 | 31 | - *Type:* string 32 | 33 | --- 34 | 35 | ##### `props`Required 36 | 37 | - *Type:* ECRDeploymentProps 38 | 39 | --- 40 | 41 | #### Methods 42 | 43 | | **Name** | **Description** | 44 | | --- | --- | 45 | | toString | Returns a string representation of this construct. | 46 | | addToPrincipalPolicy | *No description.* | 47 | 48 | --- 49 | 50 | ##### `toString` 51 | 52 | ```typescript 53 | public toString(): string 54 | ``` 55 | 56 | Returns a string representation of this construct. 57 | 58 | ##### `addToPrincipalPolicy` 59 | 60 | ```typescript 61 | public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult 62 | ``` 63 | 64 | ###### `statement`Required 65 | 66 | - *Type:* aws-cdk-lib.aws_iam.PolicyStatement 67 | 68 | --- 69 | 70 | #### Static Functions 71 | 72 | | **Name** | **Description** | 73 | | --- | --- | 74 | | isConstruct | Checks if `x` is a construct. | 75 | 76 | --- 77 | 78 | ##### ~~`isConstruct`~~ 79 | 80 | ```typescript 81 | import { ECRDeployment } from 'cdk-ecr-deployment' 82 | 83 | ECRDeployment.isConstruct(x: any) 84 | ``` 85 | 86 | Checks if `x` is a construct. 87 | 88 | ###### `x`Required 89 | 90 | - *Type:* any 91 | 92 | Any object. 93 | 94 | --- 95 | 96 | #### Properties 97 | 98 | | **Name** | **Type** | **Description** | 99 | | --- | --- | --- | 100 | | node | constructs.Node | The tree node. | 101 | 102 | --- 103 | 104 | ##### `node`Required 105 | 106 | ```typescript 107 | public readonly node: Node; 108 | ``` 109 | 110 | - *Type:* constructs.Node 111 | 112 | The tree node. 113 | 114 | --- 115 | 116 | 117 | ## Structs 118 | 119 | ### ECRDeploymentProps 120 | 121 | #### Initializer 122 | 123 | ```typescript 124 | import { ECRDeploymentProps } from 'cdk-ecr-deployment' 125 | 126 | const eCRDeploymentProps: ECRDeploymentProps = { ... } 127 | ``` 128 | 129 | #### Properties 130 | 131 | | **Name** | **Type** | **Description** | 132 | | --- | --- | --- | 133 | | dest | IImageName | The destination of the docker image. | 134 | | src | IImageName | The source of the docker image. | 135 | | imageArch | string[] | The image architecture to be copied. | 136 | | memoryLimit | number | The amount of memory (in MiB) to allocate to the AWS Lambda function which replicates the files from the CDK bucket to the destination bucket. | 137 | | role | aws-cdk-lib.aws_iam.IRole | Execution role associated with this function. | 138 | | securityGroups | aws-cdk-lib.aws_ec2.SecurityGroup[] | The list of security groups to associate with the Lambda's network interfaces. | 139 | | vpc | aws-cdk-lib.aws_ec2.IVpc | The VPC network to place the deployment lambda handler in. | 140 | | vpcSubnets | aws-cdk-lib.aws_ec2.SubnetSelection | Where in the VPC to place the deployment lambda handler. | 141 | 142 | --- 143 | 144 | ##### `dest`Required 145 | 146 | ```typescript 147 | public readonly dest: IImageName; 148 | ``` 149 | 150 | - *Type:* IImageName 151 | 152 | The destination of the docker image. 153 | 154 | --- 155 | 156 | ##### `src`Required 157 | 158 | ```typescript 159 | public readonly src: IImageName; 160 | ``` 161 | 162 | - *Type:* IImageName 163 | 164 | The source of the docker image. 165 | 166 | --- 167 | 168 | ##### `imageArch`Optional 169 | 170 | ```typescript 171 | public readonly imageArch: string[]; 172 | ``` 173 | 174 | - *Type:* string[] 175 | - *Default:* ['amd64'] 176 | 177 | The image architecture to be copied. 178 | 179 | The 'amd64' architecture will be copied by default. Specify the 180 | architecture or architectures to copy here. 181 | 182 | It is currently not possible to copy more than one architecture 183 | at a time: the array you specify must contain exactly one string. 184 | 185 | --- 186 | 187 | ##### `memoryLimit`Optional 188 | 189 | ```typescript 190 | public readonly memoryLimit: number; 191 | ``` 192 | 193 | - *Type:* number 194 | - *Default:* 512 195 | 196 | The amount of memory (in MiB) to allocate to the AWS Lambda function which replicates the files from the CDK bucket to the destination bucket. 197 | 198 | If you are deploying large files, you will need to increase this number 199 | accordingly. 200 | 201 | --- 202 | 203 | ##### `role`Optional 204 | 205 | ```typescript 206 | public readonly role: IRole; 207 | ``` 208 | 209 | - *Type:* aws-cdk-lib.aws_iam.IRole 210 | - *Default:* A role is automatically created 211 | 212 | Execution role associated with this function. 213 | 214 | --- 215 | 216 | ##### `securityGroups`Optional 217 | 218 | ```typescript 219 | public readonly securityGroups: SecurityGroup[]; 220 | ``` 221 | 222 | - *Type:* aws-cdk-lib.aws_ec2.SecurityGroup[] 223 | - *Default:* If the function is placed within a VPC and a security group is not specified, either by this or securityGroup prop, a dedicated security group will be created for this function. 224 | 225 | The list of security groups to associate with the Lambda's network interfaces. 226 | 227 | Only used if 'vpc' is supplied. 228 | 229 | --- 230 | 231 | ##### `vpc`Optional 232 | 233 | ```typescript 234 | public readonly vpc: IVpc; 235 | ``` 236 | 237 | - *Type:* aws-cdk-lib.aws_ec2.IVpc 238 | - *Default:* None 239 | 240 | The VPC network to place the deployment lambda handler in. 241 | 242 | --- 243 | 244 | ##### `vpcSubnets`Optional 245 | 246 | ```typescript 247 | public readonly vpcSubnets: SubnetSelection; 248 | ``` 249 | 250 | - *Type:* aws-cdk-lib.aws_ec2.SubnetSelection 251 | - *Default:* the Vpc default strategy if not specified 252 | 253 | Where in the VPC to place the deployment lambda handler. 254 | 255 | Only used if 'vpc' is supplied. 256 | 257 | --- 258 | 259 | ## Classes 260 | 261 | ### DockerImageName 262 | 263 | - *Implements:* IImageName 264 | 265 | #### Initializers 266 | 267 | ```typescript 268 | import { DockerImageName } from 'cdk-ecr-deployment' 269 | 270 | new DockerImageName(name: string, creds?: string) 271 | ``` 272 | 273 | | **Name** | **Type** | **Description** | 274 | | --- | --- | --- | 275 | | name | string | - The name of the image, e.g. retrieved from `DockerImageAsset.imageUri`. | 276 | | creds | string | - The credentials of the docker image. | 277 | 278 | --- 279 | 280 | ##### `name`Required 281 | 282 | - *Type:* string 283 | 284 | The name of the image, e.g. retrieved from `DockerImageAsset.imageUri`. 285 | 286 | --- 287 | 288 | ##### `creds`Optional 289 | 290 | - *Type:* string 291 | 292 | The credentials of the docker image. 293 | 294 | Format `user:password` or `AWS Secrets Manager secret arn` or `AWS Secrets Manager secret name`. 295 | If specifying an AWS Secrets Manager secret, the format of the secret should be either plain text (`user:password`) or 296 | JSON (`{"username":"","password":""}`). 297 | For more details on JSON format, see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/private-auth.html 298 | 299 | --- 300 | 301 | 302 | 303 | #### Properties 304 | 305 | | **Name** | **Type** | **Description** | 306 | | --- | --- | --- | 307 | | uri | string | The uri of the docker image. | 308 | | creds | string | - The credentials of the docker image. | 309 | 310 | --- 311 | 312 | ##### `uri`Required 313 | 314 | ```typescript 315 | public readonly uri: string; 316 | ``` 317 | 318 | - *Type:* string 319 | 320 | The uri of the docker image. 321 | 322 | The uri spec follows https://github.com/containers/skopeo 323 | 324 | --- 325 | 326 | ##### `creds`Optional 327 | 328 | ```typescript 329 | public readonly creds: string; 330 | ``` 331 | 332 | - *Type:* string 333 | 334 | The credentials of the docker image. 335 | 336 | Format `user:password` or `AWS Secrets Manager secret arn` or `AWS Secrets Manager secret name`. 337 | If specifying an AWS Secrets Manager secret, the format of the secret should be either plain text (`user:password`) or 338 | JSON (`{"username":"","password":""}`). 339 | For more details on JSON format, see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/private-auth.html 340 | 341 | --- 342 | 343 | 344 | ### S3ArchiveName 345 | 346 | - *Implements:* IImageName 347 | 348 | #### Initializers 349 | 350 | ```typescript 351 | import { S3ArchiveName } from 'cdk-ecr-deployment' 352 | 353 | new S3ArchiveName(p: string, ref?: string, creds?: string) 354 | ``` 355 | 356 | | **Name** | **Type** | **Description** | 357 | | --- | --- | --- | 358 | | p | string | - the S3 bucket name and path of the archive (a S3 URI without the s3://). | 359 | | ref | string | - appended to the end of the name with a `:`, e.g. `:latest`. | 360 | | creds | string | - The credentials of the docker image. | 361 | 362 | --- 363 | 364 | ##### `p`Required 365 | 366 | - *Type:* string 367 | 368 | the S3 bucket name and path of the archive (a S3 URI without the s3://). 369 | 370 | --- 371 | 372 | ##### `ref`Optional 373 | 374 | - *Type:* string 375 | 376 | appended to the end of the name with a `:`, e.g. `:latest`. 377 | 378 | --- 379 | 380 | ##### `creds`Optional 381 | 382 | - *Type:* string 383 | 384 | The credentials of the docker image. 385 | 386 | Format `user:password` or `AWS Secrets Manager secret arn` or `AWS Secrets Manager secret name`. 387 | If specifying an AWS Secrets Manager secret, the format of the secret should be either plain text (`user:password`) or 388 | JSON (`{"username":"","password":""}`). 389 | For more details on JSON format, see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/private-auth.html 390 | 391 | --- 392 | 393 | 394 | 395 | #### Properties 396 | 397 | | **Name** | **Type** | **Description** | 398 | | --- | --- | --- | 399 | | uri | string | The uri of the docker image. | 400 | | creds | string | - The credentials of the docker image. | 401 | 402 | --- 403 | 404 | ##### `uri`Required 405 | 406 | ```typescript 407 | public readonly uri: string; 408 | ``` 409 | 410 | - *Type:* string 411 | 412 | The uri of the docker image. 413 | 414 | The uri spec follows https://github.com/containers/skopeo 415 | 416 | --- 417 | 418 | ##### `creds`Optional 419 | 420 | ```typescript 421 | public readonly creds: string; 422 | ``` 423 | 424 | - *Type:* string 425 | 426 | The credentials of the docker image. 427 | 428 | Format `user:password` or `AWS Secrets Manager secret arn` or `AWS Secrets Manager secret name`. 429 | If specifying an AWS Secrets Manager secret, the format of the secret should be either plain text (`user:password`) or 430 | JSON (`{"username":"","password":""}`). 431 | For more details on JSON format, see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/private-auth.html 432 | 433 | --- 434 | 435 | 436 | ## Protocols 437 | 438 | ### IImageName 439 | 440 | - *Implemented By:* DockerImageName, S3ArchiveName, IImageName 441 | 442 | 443 | #### Properties 444 | 445 | | **Name** | **Type** | **Description** | 446 | | --- | --- | --- | 447 | | uri | string | The uri of the docker image. | 448 | | creds | string | The credentials of the docker image. | 449 | 450 | --- 451 | 452 | ##### `uri`Required 453 | 454 | ```typescript 455 | public readonly uri: string; 456 | ``` 457 | 458 | - *Type:* string 459 | 460 | The uri of the docker image. 461 | 462 | The uri spec follows https://github.com/containers/skopeo 463 | 464 | --- 465 | 466 | ##### `creds`Optional 467 | 468 | ```typescript 469 | public readonly creds: string; 470 | ``` 471 | 472 | - *Type:* string 473 | 474 | The credentials of the docker image. 475 | 476 | Format `user:password` or `AWS Secrets Manager secret arn` or `AWS Secrets Manager secret name`. 477 | 478 | If specifying an AWS Secrets Manager secret, the format of the secret should be either plain text (`user:password`) or 479 | JSON (`{"username":"","password":""}`). 480 | 481 | For more details on JSON format, see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/private-auth.html 482 | 483 | --- 484 | 485 | -------------------------------------------------------------------------------- /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 | cdk-ecr-deployment 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cdk-ecr-deployment 2 | 3 | [![Release](https://github.com/cdklabs/cdk-ecr-deployment/actions/workflows/release.yml/badge.svg)](https://github.com/cdklabs/cdk-ecr-deployment/actions/workflows/release.yml) 4 | [![npm version](https://img.shields.io/npm/v/cdk-ecr-deployment)](https://www.npmjs.com/package/cdk-ecr-deployment) 5 | [![PyPI](https://img.shields.io/pypi/v/cdk-ecr-deployment)](https://pypi.org/project/cdk-ecr-deployment) 6 | [![npm](https://img.shields.io/npm/dw/cdk-ecr-deployment?label=npm%20downloads)](https://www.npmjs.com/package/cdk-ecr-deployment) 7 | [![PyPI - Downloads](https://img.shields.io/pypi/dw/cdk-ecr-deployment?label=pypi%20downloads)](https://pypi.org/project/cdk-ecr-deployment) 8 | 9 | CDK construct to synchronize single docker image between docker registries. 10 | 11 | > [!IMPORTANT] 12 | > 13 | > Please use the latest version of this package, which is `v4`. 14 | > 15 | > (Older versions are no longer supported). 16 | 17 | ## Features 18 | 19 | - Copy image from ECR/external registry to (another) ECR/external registry 20 | - Copy an archive tarball image from s3 to ECR/external registry 21 | 22 | ## Examples 23 | 24 | ```ts 25 | import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; 26 | 27 | const image = new DockerImageAsset(this, 'CDKDockerImage', { 28 | directory: path.join(__dirname, 'docker'), 29 | }); 30 | 31 | // Copy from cdk docker image asset to another ECR. 32 | new ecrdeploy.ECRDeployment(this, 'DeployDockerImage1', { 33 | src: new ecrdeploy.DockerImageName(image.imageUri), 34 | dest: new ecrdeploy.DockerImageName(`${cdk.Aws.ACCOUNT_ID}.dkr.ecr.us-west-2.amazonaws.com/my-nginx:latest`), 35 | }); 36 | 37 | // Copy from docker registry to ECR. 38 | new ecrdeploy.ECRDeployment(this, 'DeployDockerImage2', { 39 | src: new ecrdeploy.DockerImageName('nginx:latest'), 40 | dest: new ecrdeploy.DockerImageName(`${cdk.Aws.ACCOUNT_ID}.dkr.ecr.us-west-2.amazonaws.com/my-nginx2:latest`), 41 | }); 42 | 43 | // Copy from private docker registry to ECR. 44 | // The format of secret in aws secrets manager must be either: 45 | // - plain text in format : 46 | // - json in format {"username":"","password":""} 47 | new ecrdeploy.ECRDeployment(this, 'DeployDockerImage3', { 48 | src: new ecrdeploy.DockerImageName('javacs3/nginx:latest', 'username:password'), 49 | // src: new ecrdeploy.DockerImageName('javacs3/nginx:latest', 'aws-secrets-manager-secret-name'), 50 | // src: new ecrdeploy.DockerImageName('javacs3/nginx:latest', 'arn:aws:secretsmanager:us-west-2:000000000000:secret:id'), 51 | dest: new ecrdeploy.DockerImageName(`${cdk.Aws.ACCOUNT_ID}.dkr.ecr.us-west-2.amazonaws.com/my-nginx3:latest`), 52 | }).addToPrincipalPolicy(new iam.PolicyStatement({ 53 | effect: iam.Effect.ALLOW, 54 | actions: [ 55 | 'secretsmanager:GetSecretValue', 56 | ], 57 | resources: ['*'], 58 | })); 59 | ``` 60 | 61 | ## Sample: [test/example.ecr-deployment.ts](./test/example.ecr-deployment.ts) 62 | 63 | After cloning the repository, install dependencies and run a full build: 64 | 65 | ```console 66 | yarn --frozen-lockfile --check-files 67 | yarn build 68 | ``` 69 | 70 | Then run the example like this: 71 | 72 | ```shell 73 | # Run the following command to try the sample. 74 | npx cdk deploy -a "npx ts-node -P tsconfig.dev.json --prefer-ts-exts test/example.ecr-deployment.ts" 75 | ``` 76 | 77 | To run the DockerHub example you will first need to setup a Secret in AWS Secrets Manager to provide DockerHub credentials. 78 | Replace `username:access-token` with your credentials. 79 | **Please note that Secrets will occur a cost.** 80 | 81 | ```console 82 | aws secretsmanager create-secret --name DockerHubCredentials --secret-string "username:access-token" 83 | ``` 84 | 85 | From the output, copy the ARN of your new secret and export it as env variable 86 | 87 | ```console 88 | export DOCKERHUB_SECRET_ARN="" 89 | ``` 90 | 91 | Finally run: 92 | 93 | ```shell 94 | # Run the following command to try the sample. 95 | npx cdk deploy -a "npx ts-node -P tsconfig.dev.json --prefer-ts-exts test/dockerhub-example.ecr-deployment.ts" 96 | ``` 97 | 98 | If your Secret is encrypted, you might have to adjust the example to also grant decrypt permissions. 99 | 100 | ## [API](./API.md) 101 | 102 | ## Tech Details & Contribution 103 | 104 | The core of this project relies on [containers/image](https://github.com/containers/image) which is used by [Skopeo](https://github.com/containers/skopeo). 105 | Please take a look at those projects before contribution. 106 | 107 | To support a new docker image source(like docker tarball in s3), you need to implement [image transport interface](https://github.com/containers/image/blob/master/types/types.go). You could take a look at [docker-archive](https://github.com/containers/image/blob/ccb87a8d0f45cf28846e307eb0ec2b9d38a458c2/docker/archive/transport.go) transport for a good start. 108 | 109 | Any error in the custom resource provider will show up in the CloudFormation error log as `Invalid PhysicalResourceId`, because of this: . You need to go into the CloudWatch Log Group to find the real error. 110 | -------------------------------------------------------------------------------- /THIRD-PARTY-LICENSES.txt: -------------------------------------------------------------------------------- 1 | ** github.com/containers/skopeo; version v1.3.0 -- 2 | https://github.com/containers/skopeo 3 | ** https://github.com/containers/image; version v5.11.0 -- 4 | https://github.com/containers/image 5 | 6 | Apache License 7 | 8 | Version 2.0, January 2004 9 | 10 | http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND 11 | DISTRIBUTION 12 | 13 | 1. Definitions. 14 | 15 | "License" shall mean the terms and conditions for use, reproduction, and 16 | distribution as defined by Sections 1 through 9 of this document. 17 | 18 | "Licensor" shall mean the copyright owner or entity authorized by the 19 | copyright owner that is granting the License. 20 | 21 | "Legal Entity" shall mean the union of the acting entity and all other 22 | entities that control, are controlled by, or are under common control 23 | with that entity. For the purposes of this definition, "control" means 24 | (i) the power, direct or indirect, to cause the direction or management 25 | of such entity, whether by contract or otherwise, or (ii) ownership of 26 | fifty percent (50%) or more of the outstanding shares, or (iii) 27 | beneficial ownership of such entity. 28 | 29 | "You" (or "Your") shall mean an individual or Legal Entity exercising 30 | permissions granted by this License. 31 | 32 | "Source" form shall mean the preferred form for making modifications, 33 | including but not limited to software source code, documentation source, 34 | and configuration files. 35 | 36 | "Object" form shall mean any form resulting from mechanical 37 | transformation or translation of a Source form, including but not limited 38 | to compiled object code, generated documentation, and conversions to 39 | other media types. 40 | 41 | "Work" shall mean the work of authorship, whether in Source or Object 42 | form, made available under the License, as indicated by a copyright 43 | notice that is included in or attached to the work (an example is 44 | provided in the Appendix below). 45 | 46 | "Derivative Works" shall mean any work, whether in Source or Object form, 47 | that is based on (or derived from) the Work and for which the editorial 48 | revisions, annotations, elaborations, or other modifications represent, 49 | as a whole, an original work of authorship. For the purposes of this 50 | License, Derivative Works shall not include works that remain separable 51 | from, or merely link (or bind by name) to the interfaces of, the Work and 52 | Derivative Works thereof. 53 | 54 | "Contribution" shall mean any work of authorship, including the original 55 | version of the Work and any modifications or additions to that Work or 56 | Derivative Works thereof, that is intentionally submitted to Licensor for 57 | inclusion in the Work by the copyright owner or by an individual or Legal 58 | Entity authorized to submit on behalf of the copyright owner. For the 59 | purposes of this definition, "submitted" means any form of electronic, 60 | verbal, or written communication sent to the Licensor or its 61 | representatives, including but not limited to communication on electronic 62 | mailing lists, source code control systems, and issue tracking systems 63 | that are managed by, or on behalf of, the Licensor for the purpose of 64 | discussing and improving the Work, but excluding communication that is 65 | conspicuously marked or otherwise designated in writing by the copyright 66 | owner as "Not a Contribution." 67 | 68 | "Contributor" shall mean Licensor and any individual or Legal Entity on 69 | behalf of whom a Contribution has been received by Licensor and 70 | subsequently incorporated within the Work. 71 | 72 | 2. Grant of Copyright License. Subject to the terms and conditions of this 73 | License, each Contributor hereby grants to You a perpetual, worldwide, 74 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to 75 | reproduce, prepare Derivative Works of, publicly display, publicly perform, 76 | sublicense, and distribute the Work and such Derivative Works in Source or 77 | Object form. 78 | 79 | 3. Grant of Patent License. Subject to the terms and conditions of this 80 | License, each Contributor hereby grants to You a perpetual, worldwide, 81 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in 82 | this section) patent license to make, have made, use, offer to sell, sell, 83 | import, and otherwise transfer the Work, where such license applies only to 84 | those patent claims licensable by such Contributor that are necessarily 85 | infringed by their Contribution(s) alone or by combination of their 86 | Contribution(s) with the Work to which such Contribution(s) was submitted. 87 | If You institute patent litigation against any entity (including a 88 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 89 | Contribution incorporated within the Work constitutes direct or contributory 90 | patent infringement, then any patent licenses granted to You under this 91 | License for that Work shall terminate as of the date such litigation is 92 | filed. 93 | 94 | 4. Redistribution. You may reproduce and distribute copies of the Work or 95 | Derivative Works thereof in any medium, with or without modifications, and 96 | in Source or Object form, provided that You meet the following conditions: 97 | 98 | (a) You must give any other recipients of the Work or Derivative Works a 99 | copy of this License; and 100 | 101 | (b) You must cause any modified files to carry prominent notices stating 102 | that You changed the files; and 103 | 104 | (c) You must retain, in the Source form of any Derivative Works that You 105 | distribute, all copyright, patent, trademark, and attribution notices 106 | from the Source form of the Work, excluding those notices that do not 107 | pertain to any part of the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must include 111 | a readable copy of the attribution notices contained within such NOTICE 112 | file, excluding those notices that do not pertain to any part of the 113 | Derivative Works, in at least one of the following places: within a 114 | NOTICE text file distributed as part of the Derivative Works; within the 115 | Source form or documentation, if provided along with the Derivative 116 | Works; or, within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents of the 118 | NOTICE file are for informational purposes only and do not modify the 119 | License. You may add Your own attribution notices within Derivative Works 120 | that You distribute, alongside or as an addendum to the NOTICE text from 121 | the Work, provided that such additional attribution notices cannot be 122 | construed as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and may 125 | provide additional or different license terms and conditions for use, 126 | reproduction, or distribution of Your modifications, or for any such 127 | Derivative Works as a whole, provided Your use, reproduction, and 128 | distribution of the Work otherwise complies with the conditions stated in 129 | this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 132 | Contribution intentionally submitted for inclusion in the Work by You to the 133 | Licensor shall be under the terms and conditions of this License, without 134 | any additional terms or conditions. Notwithstanding the above, nothing 135 | herein shall supersede or modify the terms of any separate license agreement 136 | you may have executed with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, except 140 | as required for reasonable and customary use in describing the origin of the 141 | Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 144 | writing, Licensor provides the Work (and each Contributor provides its 145 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 146 | KIND, either express or implied, including, without limitation, any 147 | warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or 148 | FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining 149 | the appropriateness of using or redistributing the Work and assume any risks 150 | associated with Your exercise of permissions under this License. 151 | 152 | 8. Limitation of Liability. In no event and under no legal theory, whether 153 | in tort (including negligence), contract, or otherwise, unless required by 154 | applicable law (such as deliberate and grossly negligent acts) or agreed to 155 | in writing, shall any Contributor be liable to You for damages, including 156 | any direct, indirect, special, incidental, or consequential damages of any 157 | character arising as a result of this License or out of the use or inability 158 | to use the Work (including but not limited to damages for loss of goodwill, 159 | work stoppage, computer failure or malfunction, or any and all other 160 | commercial damages or losses), even if such Contributor has been advised of 161 | the possibility of such damages. 162 | 163 | 9. Accepting Warranty or Additional Liability. While redistributing the Work 164 | or Derivative Works thereof, You may choose to offer, and charge a fee for, 165 | acceptance of support, warranty, indemnity, or other liability obligations 166 | and/or rights consistent with this License. However, in accepting such 167 | obligations, You may act only on Your own behalf and on Your sole 168 | responsibility, not on behalf of any other Contributor, and only if You 169 | agree to indemnify, defend, and hold each Contributor harmless for any 170 | liability incurred by, or claims asserted against, such Contributor by 171 | reason of your accepting any such warranty or additional liability. END OF 172 | TERMS AND CONDITIONS 173 | 174 | APPENDIX: How to apply the Apache License to your work. 175 | 176 | To apply the Apache License to your work, attach the following boilerplate 177 | notice, with the fields enclosed by brackets "[]" replaced with your own 178 | identifying information. (Don't include the brackets!) The text should be 179 | enclosed in the appropriate comment syntax for the file format. We also 180 | recommend that a file or class name and description of purpose be included on 181 | the same "printed page" as the copyright notice for easier identification 182 | within third-party archives. 183 | 184 | Copyright [yyyy] [name of copyright owner] 185 | 186 | Licensed under the Apache License, Version 2.0 (the "License"); 187 | 188 | you may not use this file except in compliance with the License. 189 | 190 | You may obtain a copy of the License at 191 | 192 | http://www.apache.org/licenses/LICENSE-2.0 193 | 194 | Unless required by applicable law or agreed to in writing, software 195 | 196 | distributed under the License is distributed on an "AS IS" BASIS, 197 | 198 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 199 | 200 | See the License for the specific language governing permissions and 201 | 202 | limitations under the License. 203 | 204 | * For github.com/containers/skopeo see also this required NOTICE: 205 | No copyright notice was found for this project. 206 | * For https://github.com/containers/image see also this required NOTICE: 207 | No copyright notice was found for this project. -------------------------------------------------------------------------------- /build-lambda.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | GOPROXY=${GOPROXY:-https://goproxy.io|https://goproxy.cn|direct} 5 | 6 | # The build works as follows: 7 | # 8 | # Build the given Dockerfile to produce a file in a predefined location. 9 | # We then start that container to run a single command to copy that file out, according to 10 | # the CDK Asset Bundling protocol. 11 | ${CDK_DOCKER:-docker} build -t cdk-ecr-deployment-lambda --build-arg GOPROXY="${GOPROXY}" lambda-src 12 | ${CDK_DOCKER:-docker} run --rm -v $PWD/lambda-bin:/out cdk-ecr-deployment-lambda cp /asset/bootstrap /out 13 | -------------------------------------------------------------------------------- /lambda-bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cdklabs/cdk-ecr-deployment/94ae44f68face729f1c3f2fcc62a40cf4712ce2c/lambda-bin/.gitkeep -------------------------------------------------------------------------------- /lambda-src/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | Dockerfile* 3 | node_modules 4 | coverage 5 | test-reports 6 | **/*.md 7 | bootstrap 8 | bootstrap.sha256 9 | cdk.out -------------------------------------------------------------------------------- /lambda-src/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | ARG buildImage=public.ecr.aws/docker/library/golang:1 4 | FROM ${buildImage} AS build 5 | 6 | USER root 7 | 8 | ARG GOPROXY 9 | 10 | ENV GOOS=linux \ 11 | GOARCH=amd64 \ 12 | GO111MODULE=on \ 13 | GOPROXY="${GOPROXY}" 14 | 15 | WORKDIR /ws 16 | 17 | COPY go.mod go.sum ./ 18 | 19 | RUN go env 20 | 21 | # RUN go mod download -x 22 | 23 | COPY . /ws 24 | 25 | RUN mkdir -p /asset/ && \ 26 | make OUTPUT=/asset/bootstrap 27 | -------------------------------------------------------------------------------- /lambda-src/Makefile: -------------------------------------------------------------------------------- 1 | # Taken from https://github.com/containers/skopeo 2 | # Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | .PHONY: all lambda test upgrade-deps 5 | 6 | GPGME_ENV := CGO_CFLAGS="$(shell gpgme-config --cflags 2>/dev/null)" CGO_LDFLAGS="$(shell gpgme-config --libs 2>/dev/null)" 7 | GO ?= go 8 | GOOS ?= $(shell go env GOOS) 9 | GOARCH ?= $(shell go env GOARCH) 10 | 11 | ifeq ($(DEBUG), 1) 12 | override GOGCFLAGS += -N -l 13 | endif 14 | 15 | ifeq ($(GOOS), linux) 16 | ifneq ($(GOARCH),$(filter $(GOARCH),mips mipsle mips64 mips64le ppc64 riscv64)) 17 | GO_DYN_FLAGS="-buildmode=pie" 18 | endif 19 | endif 20 | 21 | BUILDTAGS := exclude_graphdriver_devicemapper exclude_graphdriver_btrfs containers_image_openpgp lambda.norpc 22 | OUTPUT ?= cdk-ecr-deployment-handler 23 | 24 | all: lambda test 25 | 26 | upgrade-deps: 27 | CGO_ENABLED=0 $(GPGME_ENV) $(GO) get -u -tags "$(BUILDTAGS)" 28 | 29 | lambda: 30 | CGO_ENABLED=0 $(GPGME_ENV) $(GO) build -v ${GO_DYN_FLAGS} -gcflags "$(GOGCFLAGS)" -tags "$(BUILDTAGS)" -o $(OUTPUT) 31 | 32 | test: 33 | CGO_ENABLED=0 $(GPGME_ENV) $(GO) test -v -tags "$(BUILDTAGS)" ./... -------------------------------------------------------------------------------- /lambda-src/go.mod: -------------------------------------------------------------------------------- 1 | module cdk-ecr-deployment-handler 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | github.com/aws/aws-lambda-go v1.47.0 7 | github.com/aws/aws-sdk-go-v2 v1.36.3 8 | github.com/aws/aws-sdk-go-v2/config v1.29.9 9 | github.com/aws/aws-sdk-go-v2/service/ecr v1.43.0 10 | github.com/aws/aws-sdk-go-v2/service/s3 v1.78.2 11 | github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.35.2 12 | github.com/containers/image/v5 v5.34.2 13 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 14 | github.com/opencontainers/go-digest v1.0.0 15 | github.com/pkg/errors v0.9.1 16 | github.com/sirupsen/logrus v1.9.3 17 | github.com/stretchr/testify v1.10.0 18 | ) 19 | 20 | require ( 21 | dario.cat/mergo v1.0.1 // indirect 22 | github.com/BurntSushi/toml v1.4.0 // indirect 23 | github.com/Microsoft/go-winio v0.6.2 // indirect 24 | github.com/Microsoft/hcsshim v0.12.9 // indirect 25 | github.com/VividCortex/ewma v1.2.0 // indirect 26 | github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect 27 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 28 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect 29 | github.com/aws/aws-sdk-go-v2/credentials v1.17.62 // indirect 30 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 31 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 32 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 33 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 34 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect 35 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 36 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect 37 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 38 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect 39 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect 40 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect 41 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect 42 | github.com/aws/smithy-go v1.22.2 // indirect 43 | github.com/containerd/cgroups/v3 v3.0.3 // indirect 44 | github.com/containerd/errdefs v0.3.0 // indirect 45 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 46 | github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect 47 | github.com/containerd/typeurl/v2 v2.2.3 // indirect 48 | github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect 49 | github.com/containers/ocicrypt v1.2.1 // indirect 50 | github.com/containers/storage v1.57.2 // indirect 51 | github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f // indirect 52 | github.com/cyphar/filepath-securejoin v0.3.6 // indirect 53 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 54 | github.com/distribution/reference v0.6.0 // indirect 55 | github.com/docker/distribution v2.8.3+incompatible // indirect 56 | github.com/docker/docker v27.5.1+incompatible // indirect 57 | github.com/docker/docker-credential-helpers v0.8.2 // indirect 58 | github.com/docker/go-connections v0.5.0 // indirect 59 | github.com/docker/go-units v0.5.0 // indirect 60 | github.com/felixge/httpsnoop v1.0.4 // indirect 61 | github.com/go-jose/go-jose/v4 v4.0.5 // indirect 62 | github.com/go-logr/logr v1.4.2 // indirect 63 | github.com/go-logr/stdr v1.2.2 // indirect 64 | github.com/go-openapi/analysis v0.23.0 // indirect 65 | github.com/go-openapi/errors v0.22.0 // indirect 66 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 67 | github.com/go-openapi/jsonreference v0.21.0 // indirect 68 | github.com/go-openapi/loads v0.22.0 // indirect 69 | github.com/go-openapi/runtime v0.28.0 // indirect 70 | github.com/go-openapi/spec v0.21.0 // indirect 71 | github.com/go-openapi/strfmt v0.23.0 // indirect 72 | github.com/go-openapi/swag v0.23.0 // indirect 73 | github.com/go-openapi/validate v0.24.0 // indirect 74 | github.com/gogo/protobuf v1.3.2 // indirect 75 | github.com/golang/protobuf v1.5.4 // indirect 76 | github.com/google/go-containerregistry v0.20.2 // indirect 77 | github.com/google/go-intervals v0.0.2 // indirect 78 | github.com/google/uuid v1.6.0 // indirect 79 | github.com/gorilla/mux v1.8.1 // indirect 80 | github.com/hashicorp/errwrap v1.1.0 // indirect 81 | github.com/hashicorp/go-multierror v1.1.1 // indirect 82 | github.com/josharian/intern v1.0.0 // indirect 83 | github.com/json-iterator/go v1.1.12 // indirect 84 | github.com/klauspost/compress v1.17.11 // indirect 85 | github.com/klauspost/pgzip v1.2.6 // indirect 86 | github.com/letsencrypt/boulder v0.0.0-20240620165639-de9c06129bec // indirect 87 | github.com/mailru/easyjson v0.7.7 // indirect 88 | github.com/mattn/go-runewidth v0.0.16 // indirect 89 | github.com/mattn/go-sqlite3 v1.14.24 // indirect 90 | github.com/miekg/pkcs11 v1.1.1 // indirect 91 | github.com/mistifyio/go-zfs/v3 v3.0.1 // indirect 92 | github.com/mitchellh/mapstructure v1.5.0 // indirect 93 | github.com/moby/docker-image-spec v1.3.1 // indirect 94 | github.com/moby/sys/capability v0.4.0 // indirect 95 | github.com/moby/sys/mountinfo v0.7.2 // indirect 96 | github.com/moby/sys/user v0.3.0 // indirect 97 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 98 | github.com/modern-go/reflect2 v1.0.2 // indirect 99 | github.com/oklog/ulid v1.3.1 // indirect 100 | github.com/opencontainers/image-spec v1.1.0 // indirect 101 | github.com/opencontainers/runtime-spec v1.2.0 // indirect 102 | github.com/opencontainers/selinux v1.11.1 // indirect 103 | github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect 104 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 105 | github.com/proglottis/gpgme v0.1.4 // indirect 106 | github.com/rivo/uniseg v0.4.7 // indirect 107 | github.com/secure-systems-lab/go-securesystemslib v0.9.0 // indirect 108 | github.com/sigstore/fulcio v1.6.4 // indirect 109 | github.com/sigstore/rekor v1.3.8 // indirect 110 | github.com/sigstore/sigstore v1.8.12 // indirect 111 | github.com/smallstep/pkcs7 v0.1.1 // indirect 112 | github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect 113 | github.com/sylabs/sif/v2 v2.20.2 // indirect 114 | github.com/tchap/go-patricia/v2 v2.3.2 // indirect 115 | github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect 116 | github.com/ulikunitz/xz v0.5.12 // indirect 117 | github.com/vbatts/tar-split v0.11.7 // indirect 118 | github.com/vbauerster/mpb/v8 v8.9.1 // indirect 119 | go.mongodb.org/mongo-driver v1.14.0 // indirect 120 | go.opencensus.io v0.24.0 // indirect 121 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect 122 | go.opentelemetry.io/otel v1.31.0 // indirect 123 | go.opentelemetry.io/otel/metric v1.31.0 // indirect 124 | go.opentelemetry.io/otel/trace v1.31.0 // indirect 125 | golang.org/x/crypto v0.36.0 // indirect 126 | golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect 127 | golang.org/x/net v0.38.0 // indirect 128 | golang.org/x/sync v0.12.0 // indirect 129 | golang.org/x/sys v0.31.0 // indirect 130 | golang.org/x/term v0.30.0 // indirect 131 | golang.org/x/text v0.23.0 // indirect 132 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect 133 | google.golang.org/grpc v1.69.4 // indirect 134 | google.golang.org/protobuf v1.36.2 // indirect 135 | gopkg.in/yaml.v3 v3.0.1 // indirect 136 | ) 137 | -------------------------------------------------------------------------------- /lambda-src/internal/iolimits/iolimits.go: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/containers/image 2 | // Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | package iolimits 5 | 6 | import ( 7 | "io" 8 | "io/ioutil" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // All constants below are intended to be used as limits for `ReadAtMost`. The 14 | // immediate use-case for limiting the size of in-memory copied data is to 15 | // protect against OOM DOS attacks as described inCVE-2020-1702. Instead of 16 | // copying data until running out of memory, we error out after hitting the 17 | // specified limit. 18 | const ( 19 | // MegaByte denotes one megabyte and is intended to be used as a limit in 20 | // `ReadAtMost`. 21 | MegaByte = 1 << 20 22 | // MaxManifestBodySize is the maximum allowed size of a manifest. The limit 23 | // of 4 MB aligns with the one of a Docker registry: 24 | // https://github.com/docker/distribution/blob/a8371794149d1d95f1e846744b05c87f2f825e5a/registry/handlers/manifests.go#L30 25 | MaxManifestBodySize = 4 * MegaByte 26 | // MaxAuthTokenBodySize is the maximum allowed size of an auth token. 27 | // The limit of 1 MB is considered to be greatly sufficient. 28 | MaxAuthTokenBodySize = MegaByte 29 | // MaxSignatureListBodySize is the maximum allowed size of a signature list. 30 | // The limit of 4 MB is considered to be greatly sufficient. 31 | MaxSignatureListBodySize = 4 * MegaByte 32 | // MaxSignatureBodySize is the maximum allowed size of a signature. 33 | // The limit of 4 MB is considered to be greatly sufficient. 34 | MaxSignatureBodySize = 4 * MegaByte 35 | // MaxErrorBodySize is the maximum allowed size of an error-response body. 36 | // The limit of 1 MB is considered to be greatly sufficient. 37 | MaxErrorBodySize = MegaByte 38 | // MaxConfigBodySize is the maximum allowed size of a config blob. 39 | // The limit of 4 MB is considered to be greatly sufficient. 40 | MaxConfigBodySize = 4 * MegaByte 41 | // MaxOpenShiftStatusBody is the maximum allowed size of an OpenShift status body. 42 | // The limit of 4 MB is considered to be greatly sufficient. 43 | MaxOpenShiftStatusBody = 4 * MegaByte 44 | // MaxTarFileManifestSize is the maximum allowed size of a (docker save)-like manifest (which may contain multiple images) 45 | // The limit of 1 MB is considered to be greatly sufficient. 46 | MaxTarFileManifestSize = MegaByte 47 | 48 | // This size of a block 49 | BlockSize = 8 * MegaByte 50 | // The number of cache blocks 51 | CacheBlockCount = 8 52 | ) 53 | 54 | // ReadAtMost reads from reader and errors out if the specified limit (in bytes) is exceeded. 55 | func ReadAtMost(reader io.Reader, limit int) ([]byte, error) { 56 | limitedReader := io.LimitReader(reader, int64(limit+1)) 57 | 58 | res, err := ioutil.ReadAll(limitedReader) 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | if len(res) > limit { 64 | return nil, errors.Errorf("exceeded maximum allowed size of %d bytes", limit) 65 | } 66 | 67 | return res, nil 68 | } 69 | -------------------------------------------------------------------------------- /lambda-src/internal/iolimits/iolimits_test.go: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/containers/image 2 | // Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | package iolimits 5 | 6 | import ( 7 | "bytes" 8 | "math/rand" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestReadAtMost(t *testing.T) { 16 | for _, c := range []struct { 17 | input, limit int 18 | shouldSucceed bool 19 | }{ 20 | {0, 0, true}, 21 | {0, 1, true}, 22 | {1, 0, false}, 23 | {1, 1, true}, 24 | {bytes.MinRead*5 - 1, bytes.MinRead * 5, true}, 25 | {bytes.MinRead * 5, bytes.MinRead * 5, true}, 26 | {bytes.MinRead*5 + 1, bytes.MinRead * 5, false}, 27 | } { 28 | input := make([]byte, c.input) 29 | _, err := rand.Read(input) 30 | require.NoError(t, err) 31 | result, err := ReadAtMost(bytes.NewReader(input), c.limit) 32 | if c.shouldSucceed { 33 | assert.NoError(t, err) 34 | assert.Equal(t, result, input) 35 | } else { 36 | assert.Error(t, err) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lambda-src/internal/tarfile/reader.go: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/containers/image 2 | // Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | package tarfile 5 | 6 | import ( 7 | "archive/tar" 8 | "encoding/json" 9 | "io" 10 | "os" 11 | "path" 12 | 13 | "cdk-ecr-deployment-handler/internal/iolimits" 14 | 15 | "github.com/containers/image/v5/docker/reference" 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | // S3FileReader is a ((docker save)-formatted) tar archive that allows random access to any component. 20 | type S3FileReader struct { 21 | // None of the fields below are modified after the archive is created, until .Close(); 22 | // this allows concurrent readers of the same archive. 23 | s3file *S3File 24 | Manifest []ManifestItem // Guaranteed to exist after the archive is created. 25 | } 26 | 27 | // newReader creates a Reader for the specified path and removeOnClose flag. 28 | // The caller should call .Close() on the returned archive when done. 29 | func NewS3FileReader(s3file *S3File) (*S3FileReader, error) { 30 | if s3file == nil { 31 | return nil, errors.New("s3.tarfile.S3FileReader can't be nil") 32 | } 33 | 34 | // This is a valid enough archive, except Manifest is not yet filled. 35 | r := &S3FileReader{s3file: s3file} 36 | 37 | // We initialize Manifest immediately when constructing the Reader instead 38 | // of later on-demand because every caller will need the data, and because doing it now 39 | // removes the need to synchronize the access/creation of the data if the archive is later 40 | // used from multiple goroutines to access different images. 41 | 42 | // FIXME? Do we need to deal with the legacy format? 43 | bytes, err := r.readTarComponent(manifestFileName, iolimits.MegaByte) 44 | if err != nil { 45 | return nil, err 46 | } 47 | if err := json.Unmarshal(bytes, &r.Manifest); err != nil { 48 | return nil, errors.Wrap(err, "Error decoding tar manifest.json") 49 | } 50 | 51 | return r, nil 52 | } 53 | 54 | // Close removes resources associated with an initialized Reader, if any. 55 | func (r *S3FileReader) Close() error { 56 | return r.s3file.Close() 57 | } 58 | 59 | // ChooseManifestItem selects a manifest item from r.Manifest matching (ref, sourceIndex), one or 60 | // both of which should be (nil, -1). 61 | // On success, it returns the manifest item and an index of the matching tag, if a tag was used 62 | // for matching; the index is -1 if a tag was not used. 63 | func (r *S3FileReader) ChooseManifestItem(ref reference.NamedTagged, sourceIndex int) (*ManifestItem, int, error) { 64 | switch { 65 | case ref != nil && sourceIndex != -1: 66 | return nil, -1, errors.Errorf("Internal error: Cannot have both ref %s and source index @%d", 67 | ref.String(), sourceIndex) 68 | 69 | case ref != nil: 70 | refString := ref.String() 71 | for i := range r.Manifest { 72 | for tagIndex, tag := range r.Manifest[i].RepoTags { 73 | parsedTag, err := reference.ParseNormalizedNamed(tag) 74 | if err != nil { 75 | return nil, -1, errors.Wrapf(err, "Invalid tag %#v in manifest.json item @%d", tag, i) 76 | } 77 | if parsedTag.String() == refString { 78 | return &r.Manifest[i], tagIndex, nil 79 | } 80 | } 81 | } 82 | return nil, -1, errors.Errorf("Tag %#v not found", refString) 83 | 84 | case sourceIndex != -1: 85 | if sourceIndex >= len(r.Manifest) { 86 | return nil, -1, errors.Errorf("Invalid source index @%d, only %d manifest items available", 87 | sourceIndex, len(r.Manifest)) 88 | } 89 | return &r.Manifest[sourceIndex], -1, nil 90 | 91 | default: 92 | if len(r.Manifest) != 1 { 93 | return nil, -1, errors.Errorf("Unexpected tar manifest.json: expected 1 item, got %d", len(r.Manifest)) 94 | } 95 | return &r.Manifest[0], -1, nil 96 | } 97 | } 98 | 99 | // tarReadCloser is a way to close the backing file of a tar.Reader when the user no longer needs the tar component. 100 | type tarReadCloser struct { 101 | *tar.Reader 102 | } 103 | 104 | func (t *tarReadCloser) Close() error { 105 | return nil 106 | } 107 | 108 | // openTarComponent returns a ReadCloser for the specific file within the archive. 109 | // This is linear scan; we assume that the tar file will have a fairly small amount of files (~layers), 110 | // and that filesystem caching will make the repeated seeking over the (uncompressed) tarPath cheap enough. 111 | // It is safe to call this method from multiple goroutines simultaneously. 112 | // The caller should call .Close() on the returned stream. 113 | func (r *S3FileReader) openTarComponent(componentPath string) (io.ReadCloser, error) { 114 | // We must clone at here because we need to make sure each tar reader must read from the beginning. 115 | // And the internal rcache should be shared. 116 | f := r.s3file.Clone() 117 | tarReader, header, err := findTarComponent(f, componentPath) 118 | if err != nil { 119 | return nil, err 120 | } 121 | if header == nil { 122 | return nil, os.ErrNotExist 123 | } 124 | if header.FileInfo().Mode()&os.ModeType == os.ModeSymlink { // FIXME: untested 125 | // We follow only one symlink; so no loops are possible. 126 | if _, err := f.Seek(0, io.SeekStart); err != nil { 127 | return nil, err 128 | } 129 | // The new path could easily point "outside" the archive, but we only compare it to existing tar headers without extracting the archive, 130 | // so we don't care. 131 | tarReader, header, err = findTarComponent(f, path.Join(path.Dir(componentPath), header.Linkname)) 132 | if err != nil { 133 | return nil, err 134 | } 135 | if header == nil { 136 | return nil, os.ErrNotExist 137 | } 138 | } 139 | 140 | if !header.FileInfo().Mode().IsRegular() { 141 | return nil, errors.Errorf("Error reading tar archive component %s: not a regular file", header.Name) 142 | } 143 | return &tarReadCloser{Reader: tarReader}, nil 144 | } 145 | 146 | // findTarComponent returns a header and a reader matching componentPath within inputFile, 147 | // or (nil, nil, nil) if not found. 148 | func findTarComponent(inputFile io.Reader, componentPath string) (*tar.Reader, *tar.Header, error) { 149 | t := tar.NewReader(inputFile) 150 | componentPath = path.Clean(componentPath) 151 | for { 152 | h, err := t.Next() 153 | if err == io.EOF { 154 | break 155 | } 156 | if err != nil { 157 | return nil, nil, err 158 | } 159 | if path.Clean(h.Name) == componentPath { 160 | return t, h, nil 161 | } 162 | } 163 | return nil, nil, nil 164 | } 165 | 166 | // readTarComponent returns full contents of componentPath. 167 | // It is safe to call this method from multiple goroutines simultaneously. 168 | func (r *S3FileReader) readTarComponent(path string, limit int) ([]byte, error) { 169 | file, err := r.openTarComponent(path) 170 | if err != nil { 171 | return nil, errors.Wrapf(err, "Error loading tar component %s", path) 172 | } 173 | defer file.Close() 174 | bytes, err := iolimits.ReadAtMost(file, limit) 175 | if err != nil { 176 | return nil, err 177 | } 178 | return bytes, nil 179 | } 180 | -------------------------------------------------------------------------------- /lambda-src/internal/tarfile/reader_test.go: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/containers/image 2 | // Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | package tarfile 5 | 6 | import ( 7 | "context" 8 | "log" 9 | "testing" 10 | 11 | "github.com/aws/aws-sdk-go-v2/config" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestNewS3FileReader(t *testing.T) { 16 | t.Skip() 17 | cfg, err := config.LoadDefaultConfig(context.TODO()) 18 | assert.NoError(t, err) 19 | 20 | s3uri, _ := ParseS3Uri("s3://cdk-ecr-deployment/nginx.tar") 21 | 22 | f, err := NewS3File(cfg, *s3uri) 23 | assert.NoError(t, err) 24 | 25 | log.Printf("file size: %d", f.Size()) 26 | 27 | reader, err := NewS3FileReader(f) 28 | assert.NoError(t, err) 29 | 30 | log.Printf("%+v", reader.Manifest) 31 | } 32 | -------------------------------------------------------------------------------- /lambda-src/internal/tarfile/s3file.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tarfile 5 | 6 | import ( 7 | "cdk-ecr-deployment-handler/internal/iolimits" 8 | "context" 9 | "fmt" 10 | "io" 11 | "strings" 12 | "sync" 13 | 14 | "github.com/pkg/errors" 15 | 16 | "github.com/sirupsen/logrus" 17 | 18 | "github.com/aws/aws-sdk-go-v2/aws" 19 | "github.com/aws/aws-sdk-go-v2/service/s3" 20 | "github.com/golang/groupcache/lru" 21 | ) 22 | 23 | const S3Prefix = "s3://" 24 | 25 | type S3Uri struct { 26 | Bucket string 27 | Key string 28 | } 29 | 30 | func ParseS3Uri(s string) (*S3Uri, error) { 31 | if !strings.HasPrefix(s, S3Prefix) { 32 | return nil, fmt.Errorf("s3 uri must begin with %v", S3Prefix) 33 | } 34 | s = strings.TrimPrefix(s, S3Prefix) 35 | parts := strings.SplitN(s, "/", 2) 36 | if len(parts) == 1 { 37 | return &S3Uri{ 38 | Bucket: parts[0], 39 | Key: "", 40 | }, nil 41 | } 42 | return &S3Uri{ 43 | Bucket: parts[0], 44 | Key: parts[1], 45 | }, nil 46 | } 47 | 48 | type S3File struct { 49 | s3uri S3Uri 50 | client *s3.Client 51 | i int64 // current reading index 52 | size int64 // the size of the s3 object 53 | rcache *BlockCache // read cache 54 | } 55 | 56 | // Len returns the number of bytes of the unread portion of the s3 object 57 | func (f *S3File) Len() int64 { 58 | if f.i >= f.size { 59 | return 0 60 | } 61 | return f.size - f.i 62 | } 63 | 64 | // Size returns the original length of the s3 object 65 | func (f *S3File) Size() int64 { 66 | return f.size 67 | } 68 | 69 | // func (f *S3File) Read(b []byte) (n int, err error) { 70 | // logrus.Debugf("S3File: Read %d bytes", len(b)) 71 | 72 | // if f.i >= f.size { 73 | // return 0, io.EOF 74 | // } 75 | // out, err := f.client.GetObject(context.TODO(), &s3.GetObjectInput{ 76 | // Bucket: &f.s3uri.Bucket, 77 | // Key: &f.s3uri.Key, 78 | // Range: aws.String(fmt.Sprintf("bytes=%d-%d", f.i, f.i+int64(len(b))-1)), 79 | // }) 80 | // if err != nil { 81 | // return 0, err 82 | // } 83 | // defer out.Body.Close() 84 | 85 | // n, err = out.Body.Read(b) 86 | // f.i += int64(n) 87 | // if err == io.EOF { 88 | // return n, nil // e is EOF, so return nil explicitly 89 | // } 90 | // return 91 | // } 92 | 93 | func (f *S3File) onCacheMiss(block *Block) (err error) { 94 | if f.client == nil { 95 | return errors.New("S3File: api client is nil, did you close the file?") 96 | } 97 | bid := block.Id 98 | out, err := f.client.GetObject(context.TODO(), &s3.GetObjectInput{ 99 | Bucket: &f.s3uri.Bucket, 100 | Key: &f.s3uri.Key, 101 | Range: aws.String(fmt.Sprintf("bytes=%d-%d", bid*iolimits.BlockSize, (bid+1)*iolimits.BlockSize-1)), 102 | }) 103 | if err != nil { 104 | return err 105 | } 106 | defer out.Body.Close() 107 | 108 | i, n := 0, 0 109 | for i < iolimits.BlockSize { 110 | n, err = out.Body.Read(block.Buf[i:iolimits.BlockSize]) 111 | i += n 112 | if err != nil { 113 | break 114 | } 115 | } 116 | if err == io.EOF { 117 | return nil 118 | } 119 | return err 120 | } 121 | 122 | // Read implements the io.Reader interface. 123 | func (f *S3File) Read(b []byte) (n int, err error) { 124 | logrus.Debugf("S3File: Read %d bytes", len(b)) 125 | 126 | if f.i >= f.size { 127 | return 0, io.EOF 128 | } 129 | if f.rcache == nil { 130 | return 0, errors.New("S3File: rcache is nil, did you close the file?") 131 | } 132 | buf, err := f.rcache.Read(f.i, f.i+int64(len(b)), f.onCacheMiss) 133 | if err != nil { 134 | return 0, err 135 | } 136 | n = copy(b, buf) 137 | f.i += int64(n) 138 | return n, nil 139 | } 140 | 141 | // ReadAt implements the io.ReaderAt interface. 142 | func (f *S3File) ReadAt(b []byte, off int64) (n int, err error) { 143 | logrus.Debugf("S3File: ReadAt %d bytes %d offset", len(b), off) 144 | 145 | if off < 0 { 146 | return 0, errors.New("S3File: negative offset") 147 | } 148 | if off >= f.size { 149 | return 0, io.EOF 150 | } 151 | if f.rcache == nil { 152 | return 0, errors.New("S3File: rcache is nil, did you close the file?") 153 | } 154 | buf, err := f.rcache.Read(off, off+int64(len(b)), f.onCacheMiss) 155 | if err != nil { 156 | return 0, err 157 | } 158 | return copy(b, buf), nil 159 | } 160 | 161 | // Seek implements the io.Seeker interface. 162 | func (f *S3File) Seek(offset int64, whence int) (int64, error) { 163 | logrus.Debugf("S3File: Seek %d offset %d whence", offset, whence) 164 | 165 | var abs int64 166 | switch whence { 167 | case io.SeekStart: 168 | abs = offset 169 | case io.SeekCurrent: 170 | abs = f.i + offset 171 | case io.SeekEnd: 172 | abs = f.size + offset 173 | default: 174 | return 0, errors.New("S3File: invalid whence") 175 | } 176 | if abs < 0 { 177 | return 0, errors.New("S3File: negative position") 178 | } 179 | f.i = abs 180 | return abs, nil 181 | } 182 | 183 | func (f *S3File) Reset() { 184 | f.i = 0 185 | } 186 | 187 | func (f *S3File) Close() error { 188 | f.client = nil 189 | f.rcache = nil 190 | return nil 191 | } 192 | 193 | func (f *S3File) Clone() *S3File { 194 | return &S3File{ 195 | s3uri: f.s3uri, 196 | client: f.client, 197 | i: 0, 198 | size: f.size, 199 | rcache: f.rcache, 200 | } 201 | } 202 | 203 | // WriteTo implements the io.WriterTo interface. 204 | // func (f *S3File) WriteTo(w io.Writer) (n int64, err error) { 205 | // logrus.Debugf("S3File: WriteTo") 206 | 207 | // if f.i >= f.size { 208 | // return 0, io.EOF 209 | // } 210 | 211 | // wa, ok := w.(io.WriterAt) 212 | // if !ok { 213 | // return 0, errors.New("S3File: writer must be io.WriterAt") 214 | // } 215 | 216 | // downloader := manager.NewDownloader(f.client) 217 | // n, err = downloader.Download(context.TODO(), wa, &s3.GetObjectInput{ 218 | // Bucket: &f.s3uri.Bucket, 219 | // Key: &f.s3uri.Key, 220 | // Range: aws.String(fmt.Sprintf("bytes=%d-", f.i)), 221 | // }) 222 | // f.i += n 223 | // return 224 | // } 225 | 226 | func NewS3File(cfg aws.Config, s3uri S3Uri) (*S3File, error) { 227 | client := s3.NewFromConfig(cfg) 228 | output, err := client.HeadObject(context.TODO(), &s3.HeadObjectInput{ 229 | Bucket: &s3uri.Bucket, 230 | Key: &s3uri.Key, 231 | }) 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | return &S3File{ 237 | s3uri: s3uri, 238 | client: client, 239 | i: 0, 240 | size: *output.ContentLength, 241 | // The total cache size is `iolimits.CacheBlockCount * iolimits.BlockSize` 242 | rcache: NewBlockCache(iolimits.CacheBlockCount), 243 | }, nil 244 | } 245 | 246 | type Block struct { 247 | Id int64 248 | Buf []byte 249 | } 250 | 251 | func (b *Block) Size() int { 252 | return len(b.Buf) 253 | } 254 | 255 | type LRUBlockPool struct { 256 | pool *sync.Pool 257 | cache *lru.Cache 258 | mutex sync.Mutex 259 | } 260 | 261 | func NewLRUBlockPool(capacity int) *LRUBlockPool { 262 | pool := &sync.Pool{ 263 | New: func() interface{} { 264 | return &Block{ 265 | Id: -1, 266 | Buf: make([]byte, iolimits.BlockSize), 267 | } 268 | }, 269 | } 270 | cache := lru.New(capacity) 271 | cache.OnEvicted = func(k lru.Key, v interface{}) { 272 | pool.Put(v) 273 | } 274 | return &LRUBlockPool{ 275 | pool: pool, 276 | cache: cache, 277 | } 278 | } 279 | 280 | func (p *LRUBlockPool) GetBlock(id int64, blockInitFn func(*Block) error) (block *Block, err error) { 281 | p.mutex.Lock() 282 | defer p.mutex.Unlock() 283 | val, hit := p.cache.Get(id) 284 | if hit { 285 | if block, ok := val.(*Block); ok { 286 | return block, nil 287 | } else { 288 | return nil, errors.New("get an invalid block from cache") 289 | } 290 | } else { 291 | logrus.Debugf("LRUBlockPool: miss block#%d", id) 292 | if (p.cache.MaxEntries != 0) && (p.cache.Len() >= p.cache.MaxEntries) { 293 | p.cache.RemoveOldest() 294 | } 295 | blk := p.pool.Get() 296 | if block, ok := blk.(*Block); ok { 297 | block.Id = id 298 | err = blockInitFn(block) 299 | p.cache.Add(id, block) 300 | return block, err 301 | } else { 302 | return nil, errors.New("get an invalid block from pool") 303 | } 304 | } 305 | } 306 | 307 | type CacheMissFn func(b *Block) error 308 | 309 | type BlockCache struct { 310 | pool *LRUBlockPool 311 | } 312 | 313 | func NewBlockCache(capacity int) *BlockCache { 314 | return &BlockCache{ 315 | pool: NewLRUBlockPool(capacity), 316 | } 317 | } 318 | 319 | func (c *BlockCache) Read(begin, end int64, cacheMissFn CacheMissFn) (buf []byte, err error) { 320 | if begin < 0 { 321 | return nil, fmt.Errorf("LRUBlockCache: negative begin") 322 | } 323 | if end < 0 { 324 | return nil, fmt.Errorf("LRUBlockCache: negative end") 325 | } 326 | if begin >= end { 327 | return nil, fmt.Errorf("LRUBlockCache: byte end must greater than byte begin") 328 | } 329 | bidBegin := begin / iolimits.BlockSize 330 | bidEnd := end / iolimits.BlockSize 331 | buf = make([]byte, 0) 332 | 333 | for bid := bidBegin; bid <= bidEnd; bid++ { 334 | b, e := blockAddressTranslation(begin, end, bid) 335 | block, err := c.pool.GetBlock(bid, cacheMissFn) 336 | if err != nil || block == nil { 337 | return nil, errors.Wrapf(err, "error when get block from pool") 338 | } 339 | buf = append(buf, block.Buf[b:e]...) 340 | } 341 | return buf, nil 342 | } 343 | 344 | // Returns the byte range of the block at the given begin and end address 345 | func blockAddressTranslation(begin, end, bid int64) (b, e int64) { 346 | b = max(begin, bid*iolimits.BlockSize) - bid*iolimits.BlockSize 347 | e = min(end, (bid+1)*iolimits.BlockSize) - bid*iolimits.BlockSize 348 | return 349 | } 350 | 351 | func max(a, b int64) int64 { 352 | if a > b { 353 | return a 354 | } 355 | return b 356 | } 357 | 358 | func min(a, b int64) int64 { 359 | if a < b { 360 | return a 361 | } 362 | return b 363 | } 364 | -------------------------------------------------------------------------------- /lambda-src/internal/tarfile/s3file_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package tarfile 5 | 6 | import ( 7 | "archive/tar" 8 | "cdk-ecr-deployment-handler/internal/iolimits" 9 | "context" 10 | "fmt" 11 | "io" 12 | "log" 13 | "testing" 14 | 15 | "github.com/aws/aws-sdk-go-v2/config" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | func TestNewS3File(t *testing.T) { 20 | t.Skip() 21 | cfg, err := config.LoadDefaultConfig(context.TODO()) 22 | assert.NoError(t, err) 23 | 24 | s3uri, _ := ParseS3Uri("s3://cdk-ecr-deployment/nginx.tar") 25 | 26 | f, err := NewS3File(cfg, *s3uri) 27 | assert.NoError(t, err) 28 | 29 | log.Printf("file size: %d", f.Size()) 30 | 31 | tr := tar.NewReader(f) 32 | for { 33 | hdr, err := tr.Next() 34 | if err == io.EOF { 35 | break // End of archive 36 | } 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | fmt.Printf("%s\n", hdr.Name) 41 | } 42 | } 43 | 44 | func TestBlockAddressTranslation(t *testing.T) { 45 | begin := int64(iolimits.BlockSize - iolimits.MegaByte) 46 | end := int64(3*iolimits.BlockSize - iolimits.MegaByte) 47 | 48 | b, e := blockAddressTranslation(begin, end, 0) 49 | assert.Equal(t, begin, b) 50 | assert.Equal(t, int64(iolimits.BlockSize), e) 51 | 52 | b, e = blockAddressTranslation(begin, end, 1) 53 | assert.Equal(t, int64(0), b) 54 | assert.Equal(t, int64(iolimits.BlockSize), e) 55 | 56 | b, e = blockAddressTranslation(begin, end, 2) 57 | assert.Equal(t, int64(0), b) 58 | assert.Equal(t, int64(iolimits.BlockSize-iolimits.MegaByte), e) 59 | } 60 | 61 | func TestBlockCache(t *testing.T) { 62 | n := 0 63 | cache := NewBlockCache(1) 64 | cacheMissFn := func(block *Block) error { 65 | n++ 66 | copy(block.Buf, magic(block.Id)) 67 | return nil 68 | } 69 | 70 | // read 0-3 bytes of block0 71 | buf, err := cache.Read(0, 3, cacheMissFn) 72 | assert.NoError(t, err) 73 | assert.Equal(t, 1, n) 74 | assert.Equal(t, magic(0), buf) 75 | 76 | // read 0-3 bytes of block0's cache 77 | buf, err = cache.Read(0, 3, cacheMissFn) 78 | assert.NoError(t, err) 79 | assert.Equal(t, 1, n) 80 | assert.Equal(t, magic(0), buf) 81 | 82 | // read 0-3 bytes of block1 83 | buf, err = cache.Read(iolimits.BlockSize, iolimits.BlockSize+3, cacheMissFn) 84 | assert.NoError(t, err) 85 | assert.Equal(t, 2, n) 86 | assert.Equal(t, magic(1), buf) 87 | 88 | // read whole block1 and 0-3 bytes of block2 89 | buf, err = cache.Read(0, iolimits.BlockSize+3, cacheMissFn) 90 | assert.NoError(t, err) 91 | assert.Equal(t, 4, n) 92 | assert.Equal(t, append(mkblk(magic(0)), magic(1)...), buf) 93 | } 94 | 95 | func TestLRUBlockPool(t *testing.T) { 96 | n := 0 97 | pool := NewLRUBlockPool(1) 98 | blockInitFn := func(block *Block) error { 99 | n++ 100 | return nil 101 | } 102 | 103 | block, err := pool.GetBlock(0, blockInitFn) 104 | assert.NoError(t, err) 105 | assert.Equal(t, 1, n) 106 | assert.Equal(t, int64(0), block.Id) 107 | assert.Equal(t, iolimits.BlockSize, block.Size()) 108 | block.Buf[0] = byte('A') 109 | 110 | block, err = pool.GetBlock(1, blockInitFn) 111 | assert.NoError(t, err) 112 | assert.Equal(t, 2, n) 113 | assert.Equal(t, int64(1), block.Id) 114 | assert.Equal(t, iolimits.BlockSize, block.Size()) 115 | assert.Equal(t, byte('A'), block.Buf[0]) 116 | block.Buf[0] = byte('B') 117 | 118 | block, err = pool.GetBlock(1, blockInitFn) 119 | assert.NoError(t, err) 120 | assert.Equal(t, 2, n) 121 | assert.Equal(t, int64(1), block.Id) 122 | assert.Equal(t, iolimits.BlockSize, block.Size()) 123 | assert.Equal(t, byte('B'), block.Buf[0]) 124 | } 125 | 126 | // Create magic bytes based on seed: [seed-1, seed, seed+1] 127 | func magic(seed int64) []byte { 128 | return []byte{byte(seed - 1), byte(seed), byte(seed + 1)} 129 | } 130 | 131 | func mkblk(init []byte) []byte { 132 | block := make([]byte, iolimits.BlockSize) 133 | copy(block[0:len(init)], init) 134 | return block 135 | } 136 | -------------------------------------------------------------------------------- /lambda-src/internal/tarfile/src.go: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/containers/image 2 | // Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | package tarfile 5 | 6 | import ( 7 | "archive/tar" 8 | "bytes" 9 | "cdk-ecr-deployment-handler/internal/iolimits" 10 | "context" 11 | "encoding/json" 12 | "io" 13 | "io/ioutil" 14 | "path" 15 | "sync" 16 | 17 | "github.com/containers/image/v5/docker/reference" 18 | "github.com/containers/image/v5/manifest" 19 | "github.com/containers/image/v5/pkg/compression" 20 | "github.com/containers/image/v5/types" 21 | digest "github.com/opencontainers/go-digest" 22 | "github.com/pkg/errors" 23 | ) 24 | 25 | // S3FileSource is a partial implementation of types.ImageSource for reading from tarPath. 26 | type S3FileSource struct { 27 | s3fileReader *S3FileReader 28 | closeArchive bool // .Close() the archive when the source is closed. 29 | // If ref is nil and sourceIndex is -1, indicates the only image in the archive. 30 | ref reference.NamedTagged // May be nil 31 | sourceIndex int // May be -1 32 | // The following data is only available after ensureCachedDataIsPresent() succeeds 33 | tarManifest *ManifestItem // nil if not available yet. 34 | configBytes []byte 35 | configDigest digest.Digest 36 | orderedDiffIDList []digest.Digest 37 | knownLayers map[digest.Digest]*layerInfo 38 | // Other state 39 | generatedManifest []byte // Private cache for GetManifest(), nil if not set yet. 40 | cacheDataLock sync.Once // Private state for ensureCachedDataIsPresent to make it concurrency-safe 41 | cacheDataResult error // Private state for ensureCachedDataIsPresent 42 | } 43 | 44 | type layerInfo struct { 45 | path string 46 | size int64 47 | } 48 | 49 | // NewSource returns a tarfile.Source for an image in the specified archive matching ref 50 | // and sourceIndex (or the only image if they are (nil, -1)). 51 | // The archive will be closed if closeArchive 52 | func NewSource(archive *S3FileReader, closeArchive bool, ref reference.NamedTagged, sourceIndex int) *S3FileSource { 53 | return &S3FileSource{ 54 | s3fileReader: archive, 55 | closeArchive: closeArchive, 56 | ref: ref, 57 | sourceIndex: sourceIndex, 58 | } 59 | } 60 | 61 | // ensureCachedDataIsPresent loads data necessary for any of the public accessors. 62 | // It is safe to call this from multi-threaded code. 63 | func (s *S3FileSource) ensureCachedDataIsPresent() error { 64 | s.cacheDataLock.Do(func() { 65 | s.cacheDataResult = s.ensureCachedDataIsPresentPrivate() 66 | }) 67 | return s.cacheDataResult 68 | } 69 | 70 | // ensureCachedDataIsPresentPrivate is a private implementation detail of ensureCachedDataIsPresent. 71 | // Call ensureCachedDataIsPresent instead. 72 | func (s *S3FileSource) ensureCachedDataIsPresentPrivate() error { 73 | tarManifest, _, err := s.s3fileReader.ChooseManifestItem(s.ref, s.sourceIndex) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | // Read and parse config. 79 | configBytes, err := s.s3fileReader.readTarComponent(tarManifest.Config, iolimits.MaxConfigBodySize) 80 | if err != nil { 81 | return err 82 | } 83 | var parsedConfig manifest.Schema2Image // There's a lot of info there, but we only really care about layer DiffIDs. 84 | if err := json.Unmarshal(configBytes, &parsedConfig); err != nil { 85 | return errors.Wrapf(err, "Error decoding tar config %s", tarManifest.Config) 86 | } 87 | if parsedConfig.RootFS == nil { 88 | return errors.Errorf("Invalid image config (rootFS is not set): %s", tarManifest.Config) 89 | } 90 | 91 | knownLayers, err := s.prepareLayerData(tarManifest, &parsedConfig) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | // Success; commit. 97 | s.tarManifest = tarManifest 98 | s.configBytes = configBytes 99 | s.configDigest = digest.FromBytes(configBytes) 100 | s.orderedDiffIDList = parsedConfig.RootFS.DiffIDs 101 | s.knownLayers = knownLayers 102 | return nil 103 | } 104 | 105 | // Close removes resources associated with an initialized Source, if any. 106 | func (s *S3FileSource) Close() error { 107 | if s.closeArchive { 108 | return s.s3fileReader.Close() 109 | } 110 | return nil 111 | } 112 | 113 | // TarManifest returns contents of manifest.json 114 | func (s *S3FileSource) TarManifest() []ManifestItem { 115 | return s.s3fileReader.Manifest 116 | } 117 | 118 | func (s *S3FileSource) prepareLayerData(tarManifest *ManifestItem, parsedConfig *manifest.Schema2Image) (map[digest.Digest]*layerInfo, error) { 119 | // Collect layer data available in manifest and config. 120 | if len(tarManifest.Layers) != len(parsedConfig.RootFS.DiffIDs) { 121 | return nil, errors.Errorf("Inconsistent layer count: %d in manifest, %d in config", len(tarManifest.Layers), len(parsedConfig.RootFS.DiffIDs)) 122 | } 123 | knownLayers := map[digest.Digest]*layerInfo{} 124 | unknownLayerSizes := map[string]*layerInfo{} // Points into knownLayers, a "to do list" of items with unknown sizes. 125 | for i, diffID := range parsedConfig.RootFS.DiffIDs { 126 | if _, ok := knownLayers[diffID]; ok { 127 | // Apparently it really can happen that a single image contains the same layer diff more than once. 128 | // In that case, the diffID validation ensures that both layers truly are the same, and it should not matter 129 | // which of the tarManifest.Layers paths is used; (docker save) actually makes the duplicates symlinks to the original. 130 | continue 131 | } 132 | layerPath := path.Clean(tarManifest.Layers[i]) 133 | if _, ok := unknownLayerSizes[layerPath]; ok { 134 | return nil, errors.Errorf("Layer tarfile %s used for two different DiffID values", layerPath) 135 | } 136 | li := &layerInfo{ // A new element in each iteration 137 | path: layerPath, 138 | size: -1, 139 | } 140 | knownLayers[diffID] = li 141 | unknownLayerSizes[layerPath] = li 142 | } 143 | 144 | // Scan the tar file to collect layer sizes. 145 | t := tar.NewReader(s.s3fileReader.s3file) 146 | for { 147 | h, err := t.Next() 148 | if err == io.EOF { 149 | break 150 | } 151 | if err != nil { 152 | return nil, err 153 | } 154 | layerPath := path.Clean(h.Name) 155 | // FIXME: Cache this data across images in Reader. 156 | if li, ok := unknownLayerSizes[layerPath]; ok { 157 | // Since GetBlob will decompress layers that are compressed we need 158 | // to do the decompression here as well, otherwise we will 159 | // incorrectly report the size. Pretty critical, since tools like 160 | // umoci always compress layer blobs. Obviously we only bother with 161 | // the slower method of checking if it's compressed. 162 | uncompressedStream, isCompressed, err := compression.AutoDecompress(t) 163 | if err != nil { 164 | return nil, errors.Wrapf(err, "Error auto-decompressing %s to determine its size", layerPath) 165 | } 166 | defer uncompressedStream.Close() 167 | 168 | uncompressedSize := h.Size 169 | if isCompressed { 170 | uncompressedSize, err = io.Copy(ioutil.Discard, uncompressedStream) 171 | if err != nil { 172 | return nil, errors.Wrapf(err, "Error reading %s to find its size", layerPath) 173 | } 174 | } 175 | li.size = uncompressedSize 176 | delete(unknownLayerSizes, layerPath) 177 | } 178 | } 179 | if len(unknownLayerSizes) != 0 { 180 | return nil, errors.Errorf("Some layer tarfiles are missing in the tarball") // This could do with a better error reporting, if this ever happened in practice. 181 | } 182 | 183 | return knownLayers, nil 184 | } 185 | 186 | // GetManifest returns the image's manifest along with its MIME type (which may be empty when it can't be determined but the manifest is available). 187 | // It may use a remote (= slow) service. 188 | // If instanceDigest is not nil, it contains a digest of the specific manifest instance to retrieve (when the primary manifest is a manifest list); 189 | // this never happens if the primary manifest is not a manifest list (e.g. if the source never returns manifest lists). 190 | // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, 191 | // as the primary manifest can not be a list, so there can be no secondary instances. 192 | func (s *S3FileSource) GetManifest(ctx context.Context, instanceDigest *digest.Digest) ([]byte, string, error) { 193 | if instanceDigest != nil { 194 | // How did we even get here? GetManifest(ctx, nil) has returned a manifest.DockerV2Schema2MediaType. 195 | return nil, "", errors.New(`Manifest lists are not supported by "docker-daemon:"`) 196 | } 197 | if s.generatedManifest == nil { 198 | if err := s.ensureCachedDataIsPresent(); err != nil { 199 | return nil, "", err 200 | } 201 | m := manifest.Schema2{ 202 | SchemaVersion: 2, 203 | MediaType: manifest.DockerV2Schema2MediaType, 204 | ConfigDescriptor: manifest.Schema2Descriptor{ 205 | MediaType: manifest.DockerV2Schema2ConfigMediaType, 206 | Size: int64(len(s.configBytes)), 207 | Digest: s.configDigest, 208 | }, 209 | LayersDescriptors: []manifest.Schema2Descriptor{}, 210 | } 211 | for _, diffID := range s.orderedDiffIDList { 212 | li, ok := s.knownLayers[diffID] 213 | if !ok { 214 | return nil, "", errors.Errorf("Internal inconsistency: Information about layer %s missing", diffID) 215 | } 216 | m.LayersDescriptors = append(m.LayersDescriptors, manifest.Schema2Descriptor{ 217 | Digest: diffID, // diffID is a digest of the uncompressed tarball 218 | MediaType: manifest.DockerV2Schema2LayerMediaType, 219 | Size: li.size, 220 | }) 221 | } 222 | manifestBytes, err := json.Marshal(&m) 223 | if err != nil { 224 | return nil, "", err 225 | } 226 | s.generatedManifest = manifestBytes 227 | } 228 | return s.generatedManifest, manifest.DockerV2Schema2MediaType, nil 229 | } 230 | 231 | // uncompressedReadCloser is an io.ReadCloser that closes both the uncompressed stream and the underlying input. 232 | type uncompressedReadCloser struct { 233 | io.Reader 234 | underlyingCloser func() error 235 | uncompressedCloser func() error 236 | } 237 | 238 | func (r uncompressedReadCloser) Close() error { 239 | var res error 240 | if err := r.uncompressedCloser(); err != nil { 241 | res = err 242 | } 243 | if err := r.underlyingCloser(); err != nil && res == nil { 244 | res = err 245 | } 246 | return res 247 | } 248 | 249 | // HasThreadSafeGetBlob indicates whether GetBlob can be executed concurrently. 250 | func (s *S3FileSource) HasThreadSafeGetBlob() bool { 251 | return false // Not supported yet 252 | } 253 | 254 | // GetBlob returns a stream for the specified blob, and the blob’s size (or -1 if unknown). 255 | // The Digest field in BlobInfo is guaranteed to be provided, Size may be -1 and MediaType may be optionally provided. 256 | // May update BlobInfoCache, preferably after it knows for certain that a blob truly exists at a specific location. 257 | func (s *S3FileSource) GetBlob(ctx context.Context, info types.BlobInfo, cache types.BlobInfoCache) (io.ReadCloser, int64, error) { 258 | if err := s.ensureCachedDataIsPresent(); err != nil { 259 | return nil, 0, err 260 | } 261 | 262 | if info.Digest == s.configDigest { // FIXME? Implement a more general algorithm matching instead of assuming sha256. 263 | return ioutil.NopCloser(bytes.NewReader(s.configBytes)), int64(len(s.configBytes)), nil 264 | } 265 | 266 | if li, ok := s.knownLayers[info.Digest]; ok { // diffID is a digest of the uncompressed tarball, 267 | underlyingStream, err := s.s3fileReader.openTarComponent(li.path) 268 | if err != nil { 269 | return nil, 0, err 270 | } 271 | closeUnderlyingStream := true 272 | defer func() { 273 | if closeUnderlyingStream { 274 | underlyingStream.Close() 275 | } 276 | }() 277 | 278 | // In order to handle the fact that digests != diffIDs (and thus that a 279 | // caller which is trying to verify the blob will run into problems), 280 | // we need to decompress blobs. This is a bit ugly, but it's a 281 | // consequence of making everything addressable by their DiffID rather 282 | // than by their digest... 283 | // 284 | // In particular, because the v2s2 manifest being generated uses 285 | // DiffIDs, any caller of GetBlob is going to be asking for DiffIDs of 286 | // layers not their _actual_ digest. The result is that copy/... will 287 | // be verifying a "digest" which is not the actual layer's digest (but 288 | // is instead the DiffID). 289 | 290 | uncompressedStream, _, err := compression.AutoDecompress(underlyingStream) 291 | if err != nil { 292 | return nil, 0, errors.Wrapf(err, "Error auto-decompressing blob %s", info.Digest) 293 | } 294 | 295 | newStream := uncompressedReadCloser{ 296 | Reader: uncompressedStream, 297 | underlyingCloser: underlyingStream.Close, 298 | uncompressedCloser: uncompressedStream.Close, 299 | } 300 | closeUnderlyingStream = false 301 | 302 | return newStream, li.size, nil 303 | } 304 | 305 | return nil, 0, errors.Errorf("Unknown blob %s", info.Digest) 306 | } 307 | 308 | // GetSignatures returns the image's signatures. It may use a remote (= slow) service. 309 | // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, 310 | // as there can be no secondary manifests. 311 | func (s *S3FileSource) GetSignatures(ctx context.Context, instanceDigest *digest.Digest) ([][]byte, error) { 312 | if instanceDigest != nil { 313 | // How did we even get here? GetManifest(ctx, nil) has returned a manifest.DockerV2Schema2MediaType. 314 | return nil, errors.Errorf(`Manifest lists are not supported by "docker-daemon:"`) 315 | } 316 | return [][]byte{}, nil 317 | } 318 | 319 | // LayerInfosForCopy returns either nil (meaning the values in the manifest are fine), or updated values for the layer 320 | // blobsums that are listed in the image's manifest. If values are returned, they should be used when using GetBlob() 321 | // to read the image's layers. 322 | // This source implementation does not support manifest lists, so the passed-in instanceDigest should always be nil, 323 | // as the primary manifest can not be a list, so there can be no secondary manifests. 324 | // The Digest field is guaranteed to be provided; Size may be -1. 325 | // WARNING: The list may contain duplicates, and they are semantically relevant. 326 | func (s *S3FileSource) LayerInfosForCopy(context.Context, *digest.Digest) ([]types.BlobInfo, error) { 327 | return nil, nil 328 | } 329 | -------------------------------------------------------------------------------- /lambda-src/internal/tarfile/types.go: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/containers/image 2 | // Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | package tarfile 5 | 6 | import ( 7 | "github.com/containers/image/v5/manifest" 8 | "github.com/opencontainers/go-digest" 9 | ) 10 | 11 | // Various data structures. 12 | 13 | // Based on github.com/docker/docker/image/tarexport/tarexport.go 14 | const ( 15 | manifestFileName = "manifest.json" 16 | legacyLayerFileName = "layer.tar" 17 | legacyConfigFileName = "json" 18 | legacyVersionFileName = "VERSION" 19 | legacyRepositoriesFileName = "repositories" 20 | ) 21 | 22 | // ManifestItem is an element of the array stored in the top-level manifest.json file. 23 | type ManifestItem struct { // NOTE: This is visible as docker/tarfile.ManifestItem, and a part of the stable API. 24 | Config string 25 | RepoTags []string 26 | Layers []string 27 | Parent imageID `json:",omitempty"` 28 | LayerSources map[digest.Digest]manifest.Schema2Descriptor `json:",omitempty"` 29 | } 30 | 31 | type imageID string 32 | -------------------------------------------------------------------------------- /lambda-src/main.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "encoding/json" 9 | "fmt" 10 | "log" 11 | "os" 12 | 13 | "github.com/containers/image/v5/copy" 14 | "github.com/containers/image/v5/signature" 15 | "github.com/containers/image/v5/transports/alltransports" 16 | "github.com/sirupsen/logrus" 17 | 18 | "github.com/aws/aws-lambda-go/cfn" 19 | "github.com/aws/aws-lambda-go/lambda" 20 | 21 | _ "cdk-ecr-deployment-handler/s3" // Install s3 transport plugin 22 | ) 23 | 24 | const EnvLogLevel = "LOG_LEVEL" 25 | 26 | func init() { 27 | s, exists := os.LookupEnv(EnvLogLevel) 28 | if !exists { 29 | logrus.SetLevel(logrus.InfoLevel) 30 | } else { 31 | lvl, err := logrus.ParseLevel(s) 32 | if err != nil { 33 | logrus.Errorf("error parsing %s: %v", EnvLogLevel, err) 34 | } 35 | logrus.SetLevel(lvl) 36 | } 37 | } 38 | 39 | func handler(ctx context.Context, event cfn.Event) (physicalResourceID string, data map[string]interface{}, err error) { 40 | physicalResourceID = event.PhysicalResourceID 41 | data = make(map[string]interface{}) 42 | 43 | log.Printf("Event: %s", Dumps(event)) 44 | 45 | if event.RequestType == cfn.RequestDelete { 46 | return physicalResourceID, data, nil 47 | } 48 | if event.RequestType == cfn.RequestCreate || event.RequestType == cfn.RequestUpdate { 49 | srcImage, err := getStrProps(event.ResourceProperties, SRC_IMAGE) 50 | if err != nil { 51 | return physicalResourceID, data, err 52 | } 53 | destImage, err := getStrProps(event.ResourceProperties, DEST_IMAGE) 54 | if err != nil { 55 | return physicalResourceID, data, err 56 | } 57 | imageArch, err := getStrPropsDefault(event.ResourceProperties, IMAGE_ARCH, "") 58 | if err != nil { 59 | return physicalResourceID, data, err 60 | } 61 | srcCreds, err := getStrPropsDefault(event.ResourceProperties, SRC_CREDS, "") 62 | if err != nil { 63 | return physicalResourceID, data, err 64 | } 65 | destCreds, err := getStrPropsDefault(event.ResourceProperties, DEST_CREDS, "") 66 | if err != nil { 67 | return physicalResourceID, data, err 68 | } 69 | 70 | srcCreds, err = parseCreds(srcCreds) 71 | if err != nil { 72 | return physicalResourceID, data, err 73 | } 74 | destCreds, err = parseCreds(destCreds) 75 | if err != nil { 76 | return physicalResourceID, data, err 77 | } 78 | 79 | log.Printf("SrcImage: %v DestImage: %v ImageArch: %v", srcImage, destImage, imageArch) 80 | 81 | srcRef, err := alltransports.ParseImageName(srcImage) 82 | if err != nil { 83 | return physicalResourceID, data, err 84 | } 85 | destRef, err := alltransports.ParseImageName(destImage) 86 | if err != nil { 87 | return physicalResourceID, data, err 88 | } 89 | 90 | srcOpts := NewImageOpts(srcImage, imageArch) 91 | srcOpts.SetCreds(srcCreds) 92 | srcCtx, err := srcOpts.NewSystemContext() 93 | if err != nil { 94 | return physicalResourceID, data, err 95 | } 96 | destOpts := NewImageOpts(destImage, imageArch) 97 | destOpts.SetCreds(destCreds) 98 | destCtx, err := destOpts.NewSystemContext() 99 | if err != nil { 100 | return physicalResourceID, data, err 101 | } 102 | 103 | ctx, cancel := newTimeoutContext() 104 | defer cancel() 105 | policyContext, err := newPolicyContext() 106 | if err != nil { 107 | return physicalResourceID, data, err 108 | } 109 | defer policyContext.Destroy() 110 | 111 | _, err = copy.Image(ctx, policyContext, destRef, srcRef, ©.Options{ 112 | ReportWriter: os.Stdout, 113 | DestinationCtx: destCtx, 114 | SourceCtx: srcCtx, 115 | }) 116 | if err != nil { 117 | // log.Printf("Copy image failed: %v", err.Error()) 118 | // return physicalResourceID, data, nil 119 | return physicalResourceID, data, fmt.Errorf("copy image failed: %s", err.Error()) 120 | } 121 | } 122 | 123 | return physicalResourceID, data, nil 124 | } 125 | 126 | func main() { 127 | lambda.Start(cfn.LambdaWrap(handler)) 128 | } 129 | 130 | func newTimeoutContext() (context.Context, context.CancelFunc) { 131 | ctx := context.Background() 132 | var cancel context.CancelFunc = func() {} 133 | return ctx, cancel 134 | } 135 | 136 | func newPolicyContext() (*signature.PolicyContext, error) { 137 | policy := &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}} 138 | return signature.NewPolicyContext(policy) 139 | } 140 | 141 | func getStrProps(m map[string]interface{}, k string) (string, error) { 142 | v := m[k] 143 | val, ok := v.(string) 144 | if ok { 145 | return val, nil 146 | } 147 | return "", fmt.Errorf("can't get %v", k) 148 | } 149 | 150 | func getStrPropsDefault(m map[string]interface{}, k string, d string) (string, error) { 151 | v := m[k] 152 | if v == nil { 153 | return d, nil 154 | } 155 | val, ok := v.(string) 156 | if ok { 157 | return val, nil 158 | } 159 | return "", fmt.Errorf("can't get %v", k) 160 | } 161 | 162 | func parseCreds(creds string) (string, error) { 163 | credsType := GetCredsType(creds) 164 | if creds == "" { 165 | return "", nil 166 | } else if (credsType == SECRET_ARN) || (credsType == SECRET_NAME) { 167 | secret, err := GetSecret(creds) 168 | if err != nil && len(secret) > 0 && json.Valid([]byte(secret)) { 169 | secret, err = ParseJsonSecret(secret) 170 | } 171 | return secret, err 172 | } else if credsType == SECRET_TEXT { 173 | return creds, nil 174 | } 175 | return "", fmt.Errorf("unkown creds type") 176 | } 177 | -------------------------------------------------------------------------------- /lambda-src/main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "log" 8 | "os" 9 | "testing" 10 | 11 | "github.com/containers/image/v5/copy" 12 | "github.com/containers/image/v5/transports/alltransports" 13 | "github.com/stretchr/testify/assert" 14 | 15 | _ "cdk-ecr-deployment-handler/s3" 16 | ) 17 | 18 | func TestMain(t *testing.T) { 19 | t.Skip() 20 | 21 | // reference format: s3://bucket/key[:docker-reference] 22 | // valid examples: 23 | // s3://bucket/key:nginx:latest 24 | // s3://bucket/key:@0 25 | 26 | srcImage := "s3://cdk-ecr-deployment/nginx.tar:nginx:latest" 27 | destImage := "dir:/tmp/nginx.dir" 28 | 29 | log.Printf("SrcImage: %v DestImage: %v", srcImage, destImage) 30 | 31 | srcRef, err := alltransports.ParseImageName(srcImage) 32 | assert.NoError(t, err) 33 | destRef, err := alltransports.ParseImageName(destImage) 34 | assert.NoError(t, err) 35 | 36 | srcOpts := NewImageOpts(srcImage, "") 37 | srcCtx, err := srcOpts.NewSystemContext() 38 | assert.NoError(t, err) 39 | destOpts := NewImageOpts(destImage, "") 40 | destCtx, err := destOpts.NewSystemContext() 41 | assert.NoError(t, err) 42 | 43 | ctx, cancel := newTimeoutContext() 44 | defer cancel() 45 | policyContext, err := newPolicyContext() 46 | assert.NoError(t, err) 47 | defer policyContext.Destroy() 48 | 49 | _, err = copy.Image(ctx, policyContext, destRef, srcRef, ©.Options{ 50 | ReportWriter: os.Stdout, 51 | DestinationCtx: destCtx, 52 | SourceCtx: srcCtx, 53 | }) 54 | assert.NoError(t, err) 55 | } 56 | 57 | func TestNewImageOpts(t *testing.T) { 58 | srcOpts := NewImageOpts("s3://cdk-ecr-deployment/nginx.tar:nginx:latest", "arm64") 59 | _, err := srcOpts.NewSystemContext() 60 | assert.NoError(t, err) 61 | destOpts := NewImageOpts("dir:/tmp/nginx.dir", "arm64") 62 | _, err = destOpts.NewSystemContext() 63 | assert.NoError(t, err) 64 | } 65 | -------------------------------------------------------------------------------- /lambda-src/s3/src.go: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/containers/image 2 | // Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | package s3 5 | 6 | import ( 7 | "cdk-ecr-deployment-handler/internal/tarfile" 8 | "context" 9 | 10 | "github.com/aws/aws-sdk-go-v2/config" 11 | "github.com/containers/image/v5/types" 12 | ) 13 | 14 | type s3ArchiveImageSource struct { 15 | *tarfile.S3FileSource 16 | ref *s3ArchiveReference 17 | } 18 | 19 | func (s *s3ArchiveImageSource) Reference() types.ImageReference { 20 | return s.ref 21 | } 22 | 23 | func newImageSource(ctx context.Context, sys *types.SystemContext, ref *s3ArchiveReference) (types.ImageSource, error) { 24 | cfg, err := config.LoadDefaultConfig(context.TODO()) 25 | if err != nil { 26 | return nil, err 27 | } 28 | f, err := tarfile.NewS3File(cfg, *ref.s3uri) 29 | if err != nil { 30 | return nil, err 31 | } 32 | reader, err := tarfile.NewS3FileReader(f) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &s3ArchiveImageSource{ 37 | S3FileSource: tarfile.NewSource(reader, false, ref.ref, ref.sourceIndex), 38 | ref: ref, 39 | }, nil 40 | } 41 | -------------------------------------------------------------------------------- /lambda-src/s3/transport.go: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/containers/image 2 | // Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | package s3 5 | 6 | import ( 7 | "cdk-ecr-deployment-handler/internal/tarfile" 8 | "context" 9 | "fmt" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/pkg/errors" 14 | 15 | "github.com/containers/image/v5/docker/reference" 16 | "github.com/containers/image/v5/image" 17 | "github.com/containers/image/v5/transports" 18 | "github.com/containers/image/v5/types" 19 | ) 20 | 21 | func init() { 22 | transports.Register(Transport) 23 | } 24 | 25 | var Transport = &s3Transport{} 26 | 27 | type s3Transport struct{} 28 | 29 | func (t *s3Transport) Name() string { 30 | return "s3" 31 | } 32 | 33 | func (t *s3Transport) ParseReference(reference string) (types.ImageReference, error) { 34 | return ParseReference(reference) 35 | } 36 | 37 | func (t *s3Transport) ValidatePolicyConfigurationScope(scope string) error { 38 | // See the explanation in archiveReference.PolicyConfigurationIdentity. 39 | return errors.New(`s3: does not support any scopes except the default "" one`) 40 | } 41 | 42 | type s3ArchiveReference struct { 43 | s3uri *tarfile.S3Uri 44 | // May be nil to read the only image in an archive, or to create an untagged image. 45 | ref reference.NamedTagged 46 | // If not -1, a zero-based index of the image in the manifest. Valid only for sources. 47 | // Must not be set if ref is set. 48 | sourceIndex int 49 | } 50 | 51 | func ParseReference(refString string) (types.ImageReference, error) { 52 | if refString == "" { 53 | return nil, errors.New("s3 reference cannot be empty") 54 | } 55 | parts := strings.SplitN(refString, ":", 2) 56 | s3uri, err := tarfile.ParseS3Uri("s3:" + parts[0]) 57 | if err != nil { 58 | return nil, err 59 | } 60 | var nt reference.NamedTagged 61 | sourceIndex := -1 62 | 63 | if len(parts) == 2 { 64 | // A :tag or :@index was specified. 65 | if len(parts[1]) > 0 && parts[1][0] == '@' { 66 | i, err := strconv.Atoi(parts[1][1:]) 67 | if err != nil { 68 | return nil, errors.Wrapf(err, "Invalid source index %s", parts[1]) 69 | } 70 | if i < 0 { 71 | return nil, errors.Errorf("Invalid source index @%d: must not be negative", i) 72 | } 73 | sourceIndex = i 74 | } else { 75 | ref, err := reference.ParseNormalizedNamed(parts[1]) 76 | if err != nil { 77 | return nil, errors.Wrapf(err, "s3 parsing reference") 78 | } 79 | ref = reference.TagNameOnly(ref) 80 | refTagged, isTagged := ref.(reference.NamedTagged) 81 | if !isTagged { // If ref contains a digest, TagNameOnly does not change it 82 | return nil, errors.Errorf("reference does not include a tag: %s", ref.String()) 83 | } 84 | nt = refTagged 85 | } 86 | } 87 | 88 | return newReference(s3uri, nt, sourceIndex) 89 | } 90 | 91 | func newReference(s3uri *tarfile.S3Uri, ref reference.NamedTagged, sourceIndex int) (types.ImageReference, error) { 92 | if ref != nil && sourceIndex != -1 { 93 | return nil, errors.Errorf("Invalid s3: reference: cannot use both a tag and a source index") 94 | } 95 | if _, isDigest := ref.(reference.Canonical); isDigest { 96 | return nil, errors.Errorf("s3 doesn't support digest references: %s", ref.String()) 97 | } 98 | if sourceIndex != -1 && sourceIndex < 0 { 99 | return nil, errors.Errorf("Invalid s3: reference: index @%d must not be negative", sourceIndex) 100 | } 101 | return &s3ArchiveReference{ 102 | s3uri: s3uri, 103 | ref: ref, 104 | sourceIndex: sourceIndex, 105 | }, nil 106 | } 107 | 108 | func (r *s3ArchiveReference) Transport() types.ImageTransport { 109 | return Transport 110 | } 111 | 112 | func (r *s3ArchiveReference) StringWithinTransport() string { 113 | if r.s3uri.Key == "" { 114 | return fmt.Sprintf("//%s", r.s3uri.Bucket) 115 | } 116 | return fmt.Sprintf("//%s/%s", r.s3uri.Bucket, r.s3uri.Key) 117 | } 118 | 119 | func (r *s3ArchiveReference) DockerReference() reference.Named { 120 | return r.ref 121 | } 122 | 123 | func (r *s3ArchiveReference) PolicyConfigurationIdentity() string { 124 | return "" 125 | } 126 | 127 | func (r *s3ArchiveReference) PolicyConfigurationNamespaces() []string { 128 | return []string{} 129 | } 130 | 131 | func (r *s3ArchiveReference) NewImage(ctx context.Context, sys *types.SystemContext) (types.ImageCloser, error) { 132 | src, err := newImageSource(ctx, sys, r) 133 | if err != nil { 134 | return nil, err 135 | } 136 | return image.FromSource(ctx, sys, src) 137 | } 138 | 139 | func (r *s3ArchiveReference) DeleteImage(ctx context.Context, sys *types.SystemContext) error { 140 | return errors.New("deleting images not implemented for s3") 141 | } 142 | 143 | func (r *s3ArchiveReference) NewImageSource(ctx context.Context, sys *types.SystemContext) (types.ImageSource, error) { 144 | return newImageSource(ctx, sys, r) 145 | } 146 | 147 | func (r *s3ArchiveReference) NewImageDestination(ctx context.Context, sys *types.SystemContext) (types.ImageDestination, error) { 148 | return nil, fmt.Errorf(`s3 locations can only be read from, not written to`) 149 | } 150 | -------------------------------------------------------------------------------- /lambda-src/s3/transport_test.go: -------------------------------------------------------------------------------- 1 | // Taken from https://github.com/containers/image 2 | // Modifications Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | package s3 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/containers/image/v5/types" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | const ( 15 | sha256digestHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" 16 | sha256digest = "@sha256:" + sha256digestHex 17 | tarFixture = "fixtures/almostempty.tar" 18 | ) 19 | 20 | func TestTransportName(t *testing.T) { 21 | assert.Equal(t, "s3", Transport.Name()) 22 | } 23 | 24 | func TestTransportParseReference(t *testing.T) { 25 | testParseReference(t, Transport.ParseReference) 26 | } 27 | 28 | func TestTransportValidatePolicyConfigurationScope(t *testing.T) { 29 | for _, scope := range []string{ // A semi-representative assortment of values; everything is rejected. 30 | "docker.io/library/busybox:notlatest", 31 | "docker.io/library/busybox", 32 | "docker.io/library", 33 | "docker.io", 34 | "", 35 | } { 36 | err := Transport.ValidatePolicyConfigurationScope(scope) 37 | assert.Error(t, err, scope) 38 | } 39 | } 40 | 41 | func TestParseReference(t *testing.T) { 42 | testParseReference(t, ParseReference) 43 | } 44 | 45 | // testParseReference is a test shared for Transport.ParseReference and ParseReference. 46 | func testParseReference(t *testing.T, fn func(string) (types.ImageReference, error)) { 47 | for _, c := range []struct { 48 | input, expectedBucket, expectedKey, expectedRef string 49 | expectedSourceIndex int 50 | }{ 51 | {"", "", "", "", -1}, // Empty input is explicitly rejected 52 | {"//bucket", "bucket", "", "", -1}, 53 | {"//bucket/a/b", "bucket", "a/b", "", -1}, 54 | {"//bucket/", "bucket", "", "", -1}, 55 | {"//hello.com/", "hello.com", "", "", -1}, 56 | {"//bucket", "bucket", "", "", -1}, 57 | {"//bucket:busybox:notlatest", "bucket", "", "docker.io/library/busybox:notlatest", -1}, // Explicit tag 58 | {"//bucket:busybox" + sha256digest, "", "", "", -1}, // Digest references are forbidden 59 | {"//bucket:busybox", "bucket", "", "docker.io/library/busybox:latest", -1}, // Default tag 60 | // A github.com/distribution/reference value can have a tag and a digest at the same time! 61 | {"//bucket:busybox:latest" + sha256digest, "", "", "", -1}, // Both tag and digest is rejected 62 | {"//bucket:docker.io/library/busybox:latest", "bucket", "", "docker.io/library/busybox:latest", -1}, // All implied reference parts explicitly specified 63 | {"//bucket:UPPERCASEISINVALID", "", "", "", -1}, // Invalid reference format 64 | {"//bucket:@", "", "", "", -1}, // Missing source index 65 | {"//bucket:@0", "bucket", "", "", 0}, // Valid source index 66 | {"//bucket:@999999", "bucket", "", "", 999999}, // Valid source index 67 | {"//bucket:@-2", "", "", "", -1}, // Negative source index 68 | {"//bucket:@-1", "", "", "", -1}, // Negative source index, using the placeholder value 69 | {"//bucket:busybox@0", "", "", "", -1}, // References and source indices can’t be combined. 70 | {"//bucket:@0:busybox", "", "", "", -1}, // References and source indices can’t be combined. 71 | } { 72 | ref, err := fn(c.input) 73 | if c.expectedBucket == "" { 74 | assert.Error(t, err, c.input) 75 | } else { 76 | require.NoError(t, err, c.input) 77 | archiveRef, ok := ref.(*s3ArchiveReference) 78 | require.True(t, ok, c.input) 79 | assert.Equal(t, c.expectedBucket, archiveRef.s3uri.Bucket, c.input) 80 | assert.Equal(t, c.expectedKey, archiveRef.s3uri.Key, c.input) 81 | if c.expectedRef == "" { 82 | assert.Nil(t, archiveRef.ref, c.input) 83 | } else { 84 | require.NotNil(t, archiveRef.ref, c.input) 85 | assert.Equal(t, c.expectedRef, archiveRef.ref.String(), c.input) 86 | } 87 | assert.Equal(t, c.expectedSourceIndex, archiveRef.sourceIndex, c.input) 88 | } 89 | } 90 | } 91 | 92 | func TestReferenceTransport(t *testing.T) { 93 | ref, err := ParseReference("//bucket/archive.tar:nginx:latest") 94 | require.NoError(t, err) 95 | assert.Equal(t, Transport, ref.Transport()) 96 | } 97 | -------------------------------------------------------------------------------- /lambda-src/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "context" 8 | "encoding/base64" 9 | "encoding/json" 10 | "fmt" 11 | "log" 12 | "regexp" 13 | "strings" 14 | "time" 15 | 16 | "github.com/aws/aws-sdk-go-v2/aws" 17 | "github.com/aws/aws-sdk-go-v2/config" 18 | "github.com/aws/aws-sdk-go-v2/service/ecr" 19 | "github.com/aws/aws-sdk-go-v2/service/secretsmanager" 20 | "github.com/containers/image/v5/types" 21 | ) 22 | 23 | const ( 24 | SRC_IMAGE string = "SrcImage" 25 | DEST_IMAGE string = "DestImage" 26 | IMAGE_ARCH string = "ImageArch" 27 | SRC_CREDS string = "SrcCreds" 28 | DEST_CREDS string = "DestCreds" 29 | ) 30 | 31 | type ECRAuth struct { 32 | Token string 33 | User string 34 | Pass string 35 | ProxyEndpoint string 36 | ExpiresAt time.Time 37 | } 38 | 39 | func GetECRRegion(uri string) string { 40 | re := regexp.MustCompile(`dkr\.ecr\.(.+?)\.`) 41 | m := re.FindStringSubmatch(uri) 42 | if m != nil { 43 | return m[1] 44 | } 45 | return "us-east-1" 46 | } 47 | 48 | func GetECRLogin(region string) ([]ECRAuth, error) { 49 | cfg, err := config.LoadDefaultConfig( 50 | context.TODO(), 51 | config.WithRegion(region), 52 | ) 53 | if err != nil { 54 | return nil, fmt.Errorf("api client configuration error: %v", err.Error()) 55 | } 56 | client := ecr.NewFromConfig(cfg) 57 | input := &ecr.GetAuthorizationTokenInput{} 58 | 59 | resp, err := client.GetAuthorizationToken(context.TODO(), input) 60 | if err != nil { 61 | return nil, fmt.Errorf("error login into ECR: %v", err.Error()) 62 | } 63 | 64 | auths := make([]ECRAuth, len(resp.AuthorizationData)) 65 | for i, auth := range resp.AuthorizationData { 66 | // extract base64 token 67 | data, err := base64.StdEncoding.DecodeString(*auth.AuthorizationToken) 68 | if err != nil { 69 | return nil, err 70 | } 71 | // extract username and password 72 | token := strings.SplitN(string(data), ":", 2) 73 | // object to pass to template 74 | auths[i] = ECRAuth{ 75 | Token: *auth.AuthorizationToken, 76 | User: token[0], 77 | Pass: token[1], 78 | ProxyEndpoint: *(auth.ProxyEndpoint), 79 | ExpiresAt: *(auth.ExpiresAt), 80 | } 81 | } 82 | return auths, nil 83 | } 84 | 85 | type ImageOpts struct { 86 | uri string 87 | requireECRLogin bool 88 | region string 89 | creds string 90 | arch string 91 | } 92 | 93 | func NewImageOpts(uri string, arch string) *ImageOpts { 94 | requireECRLogin := strings.Contains(uri, "dkr.ecr") 95 | if requireECRLogin { 96 | return &ImageOpts{uri, requireECRLogin, GetECRRegion(uri), "", arch} 97 | } else { 98 | return &ImageOpts{uri, requireECRLogin, "", "", arch} 99 | } 100 | } 101 | 102 | func (s *ImageOpts) SetRegion(region string) { 103 | s.region = region 104 | } 105 | 106 | func (s *ImageOpts) SetCreds(creds string) { 107 | s.creds = creds 108 | } 109 | 110 | func (s *ImageOpts) NewSystemContext() (*types.SystemContext, error) { 111 | ctx := &types.SystemContext{ 112 | DockerRegistryUserAgent: "ecr-deployment", 113 | DockerAuthConfig: &types.DockerAuthConfig{}, 114 | ArchitectureChoice: s.arch, 115 | } 116 | 117 | if s.creds != "" { 118 | log.Printf("Credentials login mode for %v", s.uri) 119 | 120 | token := strings.SplitN(s.creds, ":", 2) 121 | ctx.DockerAuthConfig = &types.DockerAuthConfig{ 122 | Username: token[0], 123 | } 124 | if len(token) == 2 { 125 | ctx.DockerAuthConfig.Password = token[1] 126 | } 127 | } else { 128 | if s.requireECRLogin { 129 | log.Printf("ECR auto login mode for %v", s.uri) 130 | 131 | auths, err := GetECRLogin(s.region) 132 | if err != nil { 133 | return nil, err 134 | } 135 | if len(auths) == 0 { 136 | return nil, fmt.Errorf("empty ECR login auth token list") 137 | } 138 | auth0 := auths[0] 139 | ctx.DockerAuthConfig = &types.DockerAuthConfig{ 140 | Username: auth0.User, 141 | Password: auth0.Pass, 142 | } 143 | } 144 | } 145 | return ctx, nil 146 | } 147 | 148 | func Dumps(v interface{}) string { 149 | bytes, err := json.MarshalIndent(v, "", " ") 150 | if err != nil { 151 | return fmt.Sprintf("dumps error: %s", err.Error()) 152 | } 153 | return string(bytes) 154 | } 155 | 156 | const ( 157 | SECRET_ARN = "SECRET_ARN" 158 | SECRET_NAME = "SECRET_NAME" 159 | SECRET_TEXT = "SECRET_TEXT" 160 | ) 161 | 162 | func GetCredsType(s string) string { 163 | if strings.HasPrefix(s, "arn:aws") { 164 | return SECRET_ARN 165 | } else if strings.Contains(s, ":") { 166 | return SECRET_TEXT 167 | } else { 168 | return SECRET_NAME 169 | } 170 | } 171 | 172 | func ParseJsonSecret(s string) (secret string, err error) { 173 | var jsonData map[string]interface{} 174 | jsonErr := json.Unmarshal([]byte(s), &jsonData) 175 | if jsonErr != nil { 176 | return "", fmt.Errorf("error parsing json secret: %v", jsonErr.Error()) 177 | } 178 | username, ok := jsonData["username"].(string) 179 | if !ok { 180 | return "", fmt.Errorf("error parsing username from json secret") 181 | } 182 | password, ok := jsonData["password"].(string) 183 | if !ok { 184 | return "", fmt.Errorf("error parsing password from json secret") 185 | } 186 | return fmt.Sprintf("%s:%s", username, password), nil 187 | } 188 | 189 | func GetSecret(secretId string) (secret string, err error) { 190 | cfg, err := config.LoadDefaultConfig( 191 | context.TODO(), 192 | ) 193 | log.Printf("get secret id: %s of region: %s", secretId, cfg.Region) 194 | if err != nil { 195 | return "", fmt.Errorf("api client configuration error: %v", err.Error()) 196 | } 197 | 198 | client := secretsmanager.NewFromConfig(cfg) 199 | resp, err := client.GetSecretValue(context.TODO(), &secretsmanager.GetSecretValueInput{ 200 | SecretId: aws.String(secretId), 201 | }) 202 | if err != nil { 203 | return "", fmt.Errorf("fetch secret value error: %v", err.Error()) 204 | } 205 | return *resp.SecretString, nil 206 | } 207 | -------------------------------------------------------------------------------- /lambda-src/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | package main 5 | 6 | import ( 7 | "encoding/json" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestGetECRRegion(t *testing.T) { 14 | assert.Equal(t, 15 | "us-west-2", 16 | GetECRRegion("docker://1234567890.dkr.ecr.us-west-2.amazonaws.com/test:ubuntu"), 17 | ) 18 | assert.Equal(t, 19 | "us-east-1", 20 | GetECRRegion("docker://1234567890.dkr.ecr.us-east-1.amazonaws.com/test:ubuntu"), 21 | ) 22 | assert.Equal(t, 23 | "cn-north-1", 24 | GetECRRegion("docker://1234567890.dkr.ecr.cn-north-1.amazonaws.com/test:ubuntu"), 25 | ) 26 | } 27 | 28 | func TestGetCredsType(t *testing.T) { 29 | assert.Equal(t, SECRET_ARN, GetCredsType("arn:aws:secretsmanager:us-west-2:00000:secret:fake-secret")) 30 | assert.Equal(t, SECRET_ARN, GetCredsType("arn:aws-cn:secretsmanager:cn-north-1:00000:secret:fake-secret")) 31 | assert.Equal(t, SECRET_NAME, GetCredsType("fake-secret")) 32 | assert.Equal(t, SECRET_TEXT, GetCredsType("username:password")) 33 | assert.Equal(t, SECRET_NAME, GetCredsType("")) 34 | } 35 | 36 | func TestParseJsonSecret(t *testing.T) { 37 | secretPlainText := "user_val:pass_val" 38 | isValid := json.Valid([]byte(secretPlainText)) 39 | assert.False(t, isValid) 40 | 41 | secretJson := "{\"username\":\"user_val\",\"password\":\"pass_val\"}" 42 | isValid = json.Valid([]byte(secretJson)) 43 | assert.True(t, isValid) 44 | 45 | successCase, noError := ParseJsonSecret(secretJson) 46 | assert.NoError(t, noError) 47 | assert.Equal(t, secretPlainText, successCase) 48 | 49 | failParseCase, jsonParseError := ParseJsonSecret("{\"user}") 50 | assert.Equal(t, "", failParseCase) 51 | assert.Error(t, jsonParseError) 52 | assert.Contains(t, "error parsing json secret: unexpected end of JSON input", jsonParseError.Error()) 53 | 54 | noUsernameCase, usernameError := ParseJsonSecret("{\"password\":\"pass_val\"}") 55 | assert.Equal(t, "", noUsernameCase) 56 | assert.Error(t, usernameError) 57 | assert.Contains(t, "error parsing username from json secret", usernameError.Error()) 58 | 59 | noPasswordCase, passwordError := ParseJsonSecret("{\"username\":\"user_val\"}") 60 | assert.Equal(t, "", noPasswordCase) 61 | assert.Error(t, passwordError) 62 | assert.Contains(t, "error parsing password from json secret", passwordError.Error()) 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-ecr-deployment", 3 | "description": "CDK construct to deploy docker image to Amazon ECR", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/cdklabs/cdk-ecr-deployment" 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 | "integ": "npx projen integ", 19 | "integ:update": "npx projen integ:update", 20 | "package": "npx projen package", 21 | "package-all": "npx projen package-all", 22 | "package:dotnet": "npx projen package:dotnet", 23 | "package:go": "npx projen package:go", 24 | "package:java": "npx projen package:java", 25 | "package:js": "npx projen package:js", 26 | "package:python": "npx projen package:python", 27 | "post-compile": "npx projen post-compile", 28 | "post-upgrade": "npx projen post-upgrade", 29 | "pre-compile": "npx projen pre-compile", 30 | "release": "npx projen release", 31 | "rosetta:extract": "npx projen rosetta:extract", 32 | "test": "npx projen test", 33 | "test:watch": "npx projen test:watch", 34 | "unbump": "npx projen unbump", 35 | "upgrade": "npx projen upgrade", 36 | "upgrade-cdklabs-projen-project-types": "npx projen upgrade-cdklabs-projen-project-types", 37 | "upgrade-dev-deps": "npx projen upgrade-dev-deps", 38 | "watch": "npx projen watch", 39 | "projen": "npx projen" 40 | }, 41 | "author": { 42 | "name": "Amazon Web Services", 43 | "email": "aws-cdk-dev@amazon.com", 44 | "organization": true 45 | }, 46 | "devDependencies": { 47 | "@aws-cdk/integ-runner": "latest", 48 | "@aws-cdk/integ-tests-alpha": "latest", 49 | "@stylistic/eslint-plugin": "^2", 50 | "@types/jest": "^29", 51 | "@types/node": "^18", 52 | "@typescript-eslint/eslint-plugin": "^8", 53 | "@typescript-eslint/parser": "^8", 54 | "aws-cdk-lib": "2.80.0", 55 | "cdklabs-projen-project-types": "^0.3.1", 56 | "commit-and-tag-version": "^12", 57 | "constructs": "10.0.5", 58 | "eslint": "^9", 59 | "eslint-import-resolver-typescript": "^3.10.1", 60 | "eslint-plugin-import": "^2.31.0", 61 | "jest": "^29", 62 | "jest-junit": "^16", 63 | "jsii": "5.7.x", 64 | "jsii-diff": "^1.112.0", 65 | "jsii-docgen": "^10.5.0", 66 | "jsii-pacmak": "^1.112.0", 67 | "jsii-rosetta": "^5.8.9", 68 | "projen": "^0.92.9", 69 | "ts-jest": "^29", 70 | "ts-node": "^10.9.2", 71 | "typescript": "5.7.x" 72 | }, 73 | "peerDependencies": { 74 | "aws-cdk-lib": "^2.80.0", 75 | "constructs": "^10.0.5" 76 | }, 77 | "keywords": [ 78 | "cdk" 79 | ], 80 | "main": "lib/index.js", 81 | "license": "Apache-2.0", 82 | "publishConfig": { 83 | "access": "public" 84 | }, 85 | "version": "0.0.0", 86 | "jest": { 87 | "coverageProvider": "v8", 88 | "testMatch": [ 89 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 90 | "/@(src|test)/**/__tests__/**/*.ts?(x)", 91 | "/@(projenrc)/**/*(*.)@(spec|test).ts?(x)", 92 | "/@(projenrc)/**/__tests__/**/*.ts?(x)" 93 | ], 94 | "clearMocks": true, 95 | "collectCoverage": true, 96 | "coverageReporters": [ 97 | "json", 98 | "lcov", 99 | "clover", 100 | "cobertura", 101 | "text" 102 | ], 103 | "coverageDirectory": "coverage", 104 | "coveragePathIgnorePatterns": [ 105 | "/node_modules/" 106 | ], 107 | "testPathIgnorePatterns": [ 108 | "/node_modules/" 109 | ], 110 | "watchPathIgnorePatterns": [ 111 | "/node_modules/" 112 | ], 113 | "reporters": [ 114 | "default", 115 | [ 116 | "jest-junit", 117 | { 118 | "outputDirectory": "test-reports" 119 | } 120 | ] 121 | ], 122 | "transform": { 123 | "^.+\\.[t]sx?$": [ 124 | "ts-jest", 125 | { 126 | "tsconfig": "tsconfig.dev.json" 127 | } 128 | ] 129 | } 130 | }, 131 | "types": "lib/index.d.ts", 132 | "stability": "stable", 133 | "jsii": { 134 | "outdir": "dist", 135 | "targets": { 136 | "java": { 137 | "package": "io.github.cdklabs.cdk.ecr.deployment", 138 | "maven": { 139 | "groupId": "io.github.cdklabs", 140 | "artifactId": "cdk-ecr-deployment" 141 | } 142 | }, 143 | "python": { 144 | "distName": "cdk-ecr-deployment", 145 | "module": "cdk_ecr_deployment" 146 | }, 147 | "dotnet": { 148 | "namespace": "CdklabsCdkEcrDeployment", 149 | "packageId": "CdklabsCdkEcrDeployment" 150 | }, 151 | "go": { 152 | "moduleName": "github.com/cdklabs/cdk-ecr-deployment-go" 153 | } 154 | }, 155 | "tsc": { 156 | "outDir": "lib", 157 | "rootDir": "src" 158 | } 159 | }, 160 | "jsiiRosetta": { 161 | "exampleDependencies": { 162 | "@types/node": "^18" 163 | } 164 | }, 165 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 166 | } 167 | -------------------------------------------------------------------------------- /rosetta/default.ts-fixture: -------------------------------------------------------------------------------- 1 | // Fixture with packages imported, but nothing else 2 | import * as path from 'node:path'; 3 | import { Construct } from 'constructs'; 4 | import * as ecrdeploy from 'cdk-ecr-deployment'; 5 | import { 6 | Stack, 7 | aws_iam as iam, 8 | } from 'aws-cdk-lib'; 9 | import * as cdk from 'aws-cdk-lib'; 10 | 11 | class Fixture extends Stack { 12 | constructor(scope: Construct, id: string) { 13 | super(scope, id); 14 | 15 | /// here 16 | } 17 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as path from 'path'; 5 | import { aws_ec2 as ec2, aws_iam as iam, aws_lambda as lambda, Duration, CustomResource, Token } from 'aws-cdk-lib'; 6 | import { PolicyStatement, AddToPrincipalPolicyResult } from 'aws-cdk-lib/aws-iam'; 7 | import { RuntimeFamily } from 'aws-cdk-lib/aws-lambda'; 8 | import { Construct } from 'constructs'; 9 | 10 | export interface ECRDeploymentProps { 11 | /** 12 | * The source of the docker image. 13 | */ 14 | readonly src: IImageName; 15 | 16 | /** 17 | * The destination of the docker image. 18 | */ 19 | readonly dest: IImageName; 20 | 21 | /** 22 | * The image architecture to be copied. 23 | * 24 | * The 'amd64' architecture will be copied by default. Specify the 25 | * architecture or architectures to copy here. 26 | * 27 | * It is currently not possible to copy more than one architecture 28 | * at a time: the array you specify must contain exactly one string. 29 | * 30 | * @default ['amd64'] 31 | */ 32 | readonly imageArch?: string[]; 33 | 34 | /** 35 | * The amount of memory (in MiB) to allocate to the AWS Lambda function which 36 | * replicates the files from the CDK bucket to the destination bucket. 37 | * 38 | * If you are deploying large files, you will need to increase this number 39 | * accordingly. 40 | * 41 | * @default - 512 42 | */ 43 | readonly memoryLimit?: number; 44 | 45 | /** 46 | * Execution role associated with this function 47 | * 48 | * @default - A role is automatically created 49 | */ 50 | readonly role?: iam.IRole; 51 | 52 | /** 53 | * The VPC network to place the deployment lambda handler in. 54 | * 55 | * @default - None 56 | */ 57 | readonly vpc?: ec2.IVpc; 58 | 59 | /** 60 | * Where in the VPC to place the deployment lambda handler. 61 | * Only used if 'vpc' is supplied. 62 | * 63 | * @default - the Vpc default strategy if not specified 64 | */ 65 | readonly vpcSubnets?: ec2.SubnetSelection; 66 | 67 | /** 68 | * The list of security groups to associate with the Lambda's network interfaces. 69 | * 70 | * Only used if 'vpc' is supplied. 71 | * 72 | * @default - If the function is placed within a VPC and a security group is 73 | * not specified, either by this or securityGroup prop, a dedicated security 74 | * group will be created for this function. 75 | */ 76 | readonly securityGroups?: ec2.SecurityGroup[]; 77 | } 78 | 79 | export interface IImageName { 80 | /** 81 | * The uri of the docker image. 82 | * 83 | * The uri spec follows https://github.com/containers/skopeo 84 | */ 85 | readonly uri: string; 86 | 87 | /** 88 | * The credentials of the docker image. Format `user:password` or `AWS Secrets Manager secret arn` or `AWS Secrets Manager secret name`. 89 | * 90 | * If specifying an AWS Secrets Manager secret, the format of the secret should be either plain text (`user:password`) or 91 | * JSON (`{"username":"","password":""}`). 92 | * 93 | * For more details on JSON format, see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/private-auth.html 94 | */ 95 | creds?: string; 96 | } 97 | 98 | export class DockerImageName implements IImageName { 99 | /** 100 | * @param name - The name of the image, e.g. retrieved from `DockerImageAsset.imageUri` 101 | * @param creds - The credentials of the docker image. Format `user:password` or `AWS Secrets Manager secret arn` or `AWS Secrets Manager secret name`. 102 | * If specifying an AWS Secrets Manager secret, the format of the secret should be either plain text (`user:password`) or 103 | * JSON (`{"username":"","password":""}`). 104 | * For more details on JSON format, see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/private-auth.html 105 | */ 106 | public constructor(private name: string, public creds?: string) { } 107 | public get uri(): string { return `docker://${this.name}`; } 108 | } 109 | 110 | export class S3ArchiveName implements IImageName { 111 | private name: string; 112 | 113 | /** 114 | * @param p - the S3 bucket name and path of the archive (a S3 URI without the s3://) 115 | * @param ref - appended to the end of the name with a `:`, e.g. `:latest` 116 | * @param creds - The credentials of the docker image. Format `user:password` or `AWS Secrets Manager secret arn` or `AWS Secrets Manager secret name`. 117 | * If specifying an AWS Secrets Manager secret, the format of the secret should be either plain text (`user:password`) or 118 | * JSON (`{"username":"","password":""}`). 119 | * For more details on JSON format, see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/private-auth.html 120 | */ 121 | public constructor(p: string, ref?: string, public creds?: string) { 122 | this.name = p; 123 | if (ref) { 124 | this.name += ':' + ref; 125 | } 126 | } 127 | public get uri(): string { return `s3://${this.name}`; } 128 | } 129 | 130 | export class ECRDeployment extends Construct { 131 | private handler: lambda.SingletonFunction; 132 | 133 | constructor(scope: Construct, id: string, props: ECRDeploymentProps) { 134 | super(scope, id); 135 | const memoryLimit = props.memoryLimit ?? 512; 136 | this.handler = new lambda.SingletonFunction(this, 'CustomResourceHandler', { 137 | uuid: this.renderSingletonUuid(memoryLimit), 138 | code: lambda.Code.fromAsset(path.join(__dirname, '../lambda-bin')), 139 | runtime: new lambda.Runtime('provided.al2023', RuntimeFamily.OTHER), // not using Runtime.PROVIDED_AL2023 to support older CDK versions (< 2.105.0) 140 | handler: 'bootstrap', 141 | lambdaPurpose: 'Custom::CDKECRDeployment', 142 | description: 'Custom resource handler for copying Docker images between docker registries.', 143 | timeout: Duration.minutes(15), 144 | role: props.role, 145 | memorySize: memoryLimit, 146 | vpc: props.vpc, 147 | vpcSubnets: props.vpcSubnets, 148 | securityGroups: props.securityGroups, 149 | }); 150 | 151 | const handlerRole = this.handler.role; 152 | if (!handlerRole) { throw new Error('lambda.SingletonFunction should have created a Role'); } 153 | 154 | handlerRole.addToPrincipalPolicy( 155 | new iam.PolicyStatement({ 156 | effect: iam.Effect.ALLOW, 157 | actions: [ 158 | 'ecr:GetAuthorizationToken', 159 | 'ecr:BatchCheckLayerAvailability', 160 | 'ecr:GetDownloadUrlForLayer', 161 | 'ecr:GetRepositoryPolicy', 162 | 'ecr:DescribeRepositories', 163 | 'ecr:ListImages', 164 | 'ecr:DescribeImages', 165 | 'ecr:BatchGetImage', 166 | 'ecr:ListTagsForResource', 167 | 'ecr:DescribeImageScanFindings', 168 | 'ecr:InitiateLayerUpload', 169 | 'ecr:UploadLayerPart', 170 | 'ecr:CompleteLayerUpload', 171 | 'ecr:PutImage', 172 | ], 173 | resources: ['*'], 174 | })); 175 | handlerRole.addToPrincipalPolicy(new iam.PolicyStatement({ 176 | effect: iam.Effect.ALLOW, 177 | actions: [ 178 | 's3:GetObject', 179 | ], 180 | resources: ['*'], 181 | })); 182 | 183 | if (props.imageArch && props.imageArch.length !== 1) { 184 | throw new Error(`imageArch must contain exactly 1 element, got ${JSON.stringify(props.imageArch)}`); 185 | } 186 | const imageArch = props.imageArch ? props.imageArch[0] : ''; 187 | 188 | new CustomResource(this, 'CustomResource', { 189 | serviceToken: this.handler.functionArn, 190 | // This has been copy/pasted and is a pure lie, but changing it is going to change people's infra!! X( 191 | resourceType: 'Custom::CDKECRDeployment', 192 | properties: { 193 | SrcImage: props.src.uri, 194 | SrcCreds: props.src.creds, 195 | DestImage: props.dest.uri, 196 | DestCreds: props.dest.creds, 197 | ...imageArch ? { ImageArch: imageArch } : {}, 198 | }, 199 | }); 200 | } 201 | 202 | public addToPrincipalPolicy(statement: PolicyStatement): AddToPrincipalPolicyResult { 203 | const handlerRole = this.handler.role; 204 | if (!handlerRole) { throw new Error('lambda.SingletonFunction should have created a Role'); } 205 | 206 | return handlerRole.addToPrincipalPolicy(statement); 207 | } 208 | 209 | private renderSingletonUuid(memoryLimit?: number) { 210 | let uuid = 'bd07c930-edb9-4112-a20f-03f096f53666'; 211 | 212 | // if user specify a custom memory limit, define another singleton handler 213 | // with this configuration. otherwise, it won't be possible to use multiple 214 | // configurations since we have a singleton. 215 | if (memoryLimit) { 216 | if (Token.isUnresolved(memoryLimit)) { 217 | throw new Error('Can\'t use tokens when specifying "memoryLimit" since we use it to identify the singleton custom resource handler'); 218 | } 219 | 220 | uuid += `-${memoryLimit.toString()}MiB`; 221 | } 222 | 223 | return uuid; 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /test/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | 4 | 5 | FROM nginx -------------------------------------------------------------------------------- /test/dockerhub-example.ecr-deployment.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as path from 'path'; 5 | import { 6 | aws_iam as iam, 7 | aws_ecr as ecr, 8 | aws_ecr_assets as assets, 9 | Stack, 10 | App, 11 | StackProps, 12 | RemovalPolicy, 13 | } from 'aws-cdk-lib'; 14 | import * as sm from 'aws-cdk-lib/aws-secretsmanager'; 15 | import * as ecrDeploy from '../src/index'; 16 | 17 | // Requires access to DockerHub credentials, otherwise will fail 18 | // See README.md for more details 19 | if (!process.env.DOCKERHUB_SECRET_ARN) { 20 | throw new Error('DOCKERHUB_SECRET_ARN is required, see README.md for details'); 21 | } 22 | 23 | class TestECRDeployment extends Stack { 24 | constructor(scope: App, id: string, props?: StackProps) { 25 | super(scope, id, props); 26 | 27 | const repo = new ecr.Repository(this, 'TargetRepo', { 28 | repositoryName: 'ecr-deployment-dockerhub-target', 29 | removalPolicy: RemovalPolicy.DESTROY, 30 | autoDeleteImages: true, 31 | }); 32 | 33 | 34 | const dockerHubSecret = sm.Secret.fromSecretCompleteArn(this, 'DockerHubSecret', process.env.DOCKERHUB_SECRET_ARN!); 35 | 36 | new ecrDeploy.ECRDeployment(this, 'DeployDockerImage1', { 37 | src: new ecrDeploy.DockerImageName('alpine:latest', dockerHubSecret.secretFullArn), 38 | dest: new ecrDeploy.DockerImageName(`${repo.repositoryUri}:alpine-from-dockerhub`), 39 | }).addToPrincipalPolicy(new iam.PolicyStatement({ 40 | effect: iam.Effect.ALLOW, 41 | actions: [ 42 | 'secretsmanager:GetSecretValue', 43 | ], 44 | resources: [dockerHubSecret.secretArn], 45 | })); 46 | 47 | new ecrDeploy.ECRDeployment(this, 'DeployDockerImage2', { 48 | src: new ecrDeploy.DockerImageName('alpine:latest', dockerHubSecret.secretFullArn), 49 | dest: new ecrDeploy.DockerImageName(`${repo.repositoryUri}:alpine-amd64-from-dockerhub`), 50 | imageArch: ['amd64'], 51 | }).addToPrincipalPolicy(new iam.PolicyStatement({ 52 | effect: iam.Effect.ALLOW, 53 | actions: [ 54 | 'secretsmanager:GetSecretValue', 55 | ], 56 | resources: [dockerHubSecret.secretArn], 57 | })); 58 | } 59 | } 60 | 61 | 62 | const app = new App(); 63 | 64 | new TestECRDeployment(app, 'test-ecr-deployments-dockerhub'); 65 | 66 | app.synth(); 67 | -------------------------------------------------------------------------------- /test/ecr-deployment.test.ts: -------------------------------------------------------------------------------- 1 | import { Stack, App, aws_ecr as ecr, assertions } from 'aws-cdk-lib'; 2 | import { DockerImageName, ECRDeployment } from '../src'; 3 | 4 | // Yes, it's a lie. It's also the truth. 5 | const CUSTOM_RESOURCE_TYPE = 'Custom::CDKECRDeployment'; 6 | 7 | let app: App; 8 | let stack: Stack; 9 | 10 | const src = new DockerImageName('javacs3/javacs3:latest', 'dockerhub'); 11 | let dest: DockerImageName; 12 | beforeEach(() => { 13 | app = new App(); 14 | stack = new Stack(app, 'Stack'); 15 | 16 | const repo = new ecr.Repository(stack, 'Repo', { 17 | repositoryName: 'repo', 18 | }); 19 | dest = new DockerImageName(`${repo.repositoryUri}:copied`); 20 | 21 | // Otherwise we do a Docker build :x 22 | process.env.FORCE_PREBUILT_LAMBDA = 'true'; 23 | }); 24 | 25 | test('ImageArch is missing from custom resource if argument not specified', () => { 26 | // WHEN 27 | new ECRDeployment(stack, 'ECR', { 28 | src, 29 | dest, 30 | }); 31 | 32 | // THEN 33 | const template = assertions.Template.fromStack(stack); 34 | template.hasResourceProperties(CUSTOM_RESOURCE_TYPE, { 35 | ImageArch: assertions.Match.absent(), 36 | }); 37 | }); 38 | 39 | test('ImageArch is in custom resource properties if specified', () => { 40 | // WHEN 41 | new ECRDeployment(stack, 'ECR', { 42 | src, 43 | dest, 44 | imageArch: ['banana'], 45 | }); 46 | 47 | // THEN 48 | const template = assertions.Template.fromStack(stack); 49 | template.hasResourceProperties(CUSTOM_RESOURCE_TYPE, { 50 | ImageArch: 'banana', 51 | }); 52 | }); 53 | 54 | test('Cannot specify more or fewer than 1 elements in imageArch', () => { 55 | // WHEN 56 | expect(() => new ECRDeployment(stack, 'ECR', { 57 | src, 58 | dest, 59 | imageArch: ['banana', 'pear'], 60 | })).toThrow(/imageArch must contain exactly 1 element/); 61 | }); -------------------------------------------------------------------------------- /test/example.ecr-deployment.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import * as path from 'path'; 5 | import { 6 | aws_ecr as ecr, 7 | aws_ecr_assets as assets, 8 | Stack, 9 | App, 10 | StackProps, 11 | RemovalPolicy, 12 | } from 'aws-cdk-lib'; 13 | import * as ecrDeploy from '../src/index'; 14 | 15 | class TestECRDeployment extends Stack { 16 | constructor(scope: App, id: string, props?: StackProps) { 17 | super(scope, id, props); 18 | 19 | const repo = new ecr.Repository(this, 'TargetRepo', { 20 | repositoryName: 'ecr-deployment-target', 21 | removalPolicy: RemovalPolicy.DESTROY, 22 | autoDeleteImages: true, 23 | }); 24 | 25 | const image = new assets.DockerImageAsset(this, 'CDKDockerImage', { 26 | directory: path.join(__dirname, 'docker'), 27 | platform: assets.Platform.LINUX_AMD64, 28 | }); 29 | 30 | const imageArm = new assets.DockerImageAsset(this, 'CDKDockerImageArm', { 31 | directory: path.join(__dirname, 'docker'), 32 | platform: assets.Platform.LINUX_ARM64, 33 | }); 34 | 35 | new ecrDeploy.ECRDeployment(this, 'DeployECRImage1', { 36 | src: new ecrDeploy.DockerImageName(image.imageUri), 37 | dest: new ecrDeploy.DockerImageName(`${repo.repositoryUri}:latest`), 38 | }); 39 | 40 | new ecrDeploy.ECRDeployment(this, 'DeployECRImage2', { 41 | src: new ecrDeploy.DockerImageName(imageArm.imageUri), 42 | dest: new ecrDeploy.DockerImageName(`${repo.repositoryUri}:latest-arm64`), 43 | imageArch: ['arm64'], 44 | }); 45 | 46 | // Your can also copy a docker archive image tarball from s3 47 | // new ecrDeploy.ECRDeployment(this, 'DeployDockerImage', { 48 | // src: new ecrDeploy.S3ArchiveName('bucket-name/nginx.tar', 'nginx:latest'), 49 | // dest: new ecrDeploy.DockerImageName(`${repo.repositoryUri}:latest`), 50 | // }); 51 | } 52 | } 53 | 54 | const app = new App(); 55 | 56 | new TestECRDeployment(app, 'test-ecr-deployments', { 57 | env: { account: process.env.AWS_DEFAULT_ACCOUNT, region: process.env.AWS_DEFAULT_REGION }, 58 | }); 59 | 60 | app.synth(); 61 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | 5 | import { DockerImageName, S3ArchiveName } from '../src'; 6 | 7 | test(`${DockerImageName.name}`, () => { 8 | const name = new DockerImageName('nginx:latest'); 9 | 10 | expect(name.uri).toBe('docker://nginx:latest'); 11 | }); 12 | 13 | test(`${S3ArchiveName.name}`, () => { 14 | const name = new S3ArchiveName('bucket/nginx.tar', 'nginx:latest'); 15 | 16 | expect(name.uri).toBe('s3://bucket/nginx.tar:nginx:latest'); 17 | }); -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2020" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2020" 27 | }, 28 | "include": [ 29 | "src/**/*.ts", 30 | "test/**/*.ts", 31 | ".projenrc.ts", 32 | "projenrc/**/*.ts" 33 | ], 34 | "exclude": [ 35 | "node_modules" 36 | ] 37 | } 38 | --------------------------------------------------------------------------------