├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── doc.yml │ ├── feature-request.yml │ └── general-issue.yml ├── pull_request_template.md └── workflows │ ├── auto-approve.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 ├── .mergify.yml ├── .npmignore ├── .pre-commit-config.yaml ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.ts ├── API.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── package.json ├── resources └── constructs │ └── ecsIsoServiceAutoscaler │ └── ecs_scaling_manager.py ├── rosetta └── default.ts-fixture ├── scripts └── append-api-docs-to-readme.sh ├── src ├── constructs │ ├── ecsIsoServiceAutoscaler │ │ └── ecsIsoServiceAutoscaler.ts │ ├── enterpriseDnsResolver │ │ └── enterpriseDnsResolver.ts │ └── populateProvidedVpc │ │ └── populateProvidedVpc.ts ├── index.ts ├── patches │ ├── addCfnInitProxy.ts │ ├── addLambdaEnvironmentVariables.ts │ ├── addPermissionsBoundary.ts │ ├── convertInlinePoliciesToManaged.ts │ ├── removePublicAccessBlockConfiguration.ts │ ├── removeTags.ts │ ├── resource-extractor │ │ ├── cfnStore.ts │ │ ├── flattener.ts │ │ ├── resourceExtractor.ts │ │ ├── resourceTransformer.ts │ │ └── types.ts │ └── setApiGatewayEndpointConfiguration.ts └── utils │ └── utils.ts ├── test ├── constructs │ ├── ecsIsoServiceAutoscaler │ │ ├── ecsIsoServiceAutoscaler.test.ts │ │ └── resources │ │ │ ├── Dockerfile │ │ │ ├── conftest.py │ │ │ ├── test.sh │ │ │ └── test_ecs_scaling_manager.py │ ├── enterpriseDnsResolver │ │ └── enterpriseDnsResolver.test.ts │ └── populateProvidedVpc │ │ └── populateProvidedVpc.test.ts ├── integ │ ├── patches │ │ ├── integ.addCfnInitProxy.ts │ │ └── integ.addCfnInitProxy.ts.snapshot │ │ │ ├── IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json │ │ │ ├── IntegTestDefaultTestDeployAssertE3E7D2A4.template.json │ │ │ ├── cdk.out │ │ │ ├── integ-addCfnInitProxy-stack.assets.json │ │ │ ├── integ-addCfnInitProxy-stack.template.json │ │ │ ├── integ.json │ │ │ ├── manifest.json │ │ │ └── tree.json │ └── tsconfig.json ├── patches │ ├── addCfnInitProxy.test.ts │ ├── addLambdaEnvironmentVariables.test.ts │ ├── addPermissionsBoundary.test.ts │ ├── convertInlinePoliciesToManaged.test.ts │ ├── removePublicAccessBlockConfiguration.test.ts │ ├── removeTags.test.ts │ ├── resource-extractor │ │ ├── cfnStore.test.ts │ │ ├── flattener.test.ts │ │ ├── resourceExtractor.test.ts │ │ └── resourceTransformer.test.ts │ └── setApiGatewayEndpointConfiguration.test.ts └── utils │ └── utils.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 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "project": "./tsconfig.dev.json" 17 | }, 18 | "extends": [ 19 | "plugin:import/typescript", 20 | "plugin:security/recommended-legacy", 21 | "plugin:prettier/recommended" 22 | ], 23 | "settings": { 24 | "import/parsers": { 25 | "@typescript-eslint/parser": [ 26 | ".ts", 27 | ".tsx" 28 | ] 29 | }, 30 | "import/resolver": { 31 | "node": {}, 32 | "typescript": { 33 | "project": "./tsconfig.dev.json", 34 | "alwaysTryTypes": true 35 | } 36 | } 37 | }, 38 | "ignorePatterns": [ 39 | "*.js", 40 | "*.d.ts", 41 | "node_modules/", 42 | "*.generated.ts", 43 | "coverage", 44 | "!.projenrc.ts", 45 | "!projenrc/**/*.ts" 46 | ], 47 | "rules": { 48 | "curly": [ 49 | "error", 50 | "multi-line", 51 | "consistent" 52 | ], 53 | "@typescript-eslint/no-require-imports": "error", 54 | "import/no-extraneous-dependencies": [ 55 | "error", 56 | { 57 | "devDependencies": [ 58 | "**/test/**", 59 | "**/build-tools/**", 60 | ".projenrc.ts", 61 | "projenrc/**/*.ts" 62 | ], 63 | "optionalDependencies": false, 64 | "peerDependencies": true 65 | } 66 | ], 67 | "import/no-unresolved": [ 68 | "error" 69 | ], 70 | "import/order": [ 71 | "warn", 72 | { 73 | "groups": [ 74 | "builtin", 75 | "external" 76 | ], 77 | "alphabetize": { 78 | "order": "asc", 79 | "caseInsensitive": true 80 | } 81 | } 82 | ], 83 | "import/no-duplicates": [ 84 | "error" 85 | ], 86 | "no-shadow": [ 87 | "off" 88 | ], 89 | "@typescript-eslint/no-shadow": "error", 90 | "@typescript-eslint/no-floating-promises": "error", 91 | "no-return-await": [ 92 | "off" 93 | ], 94 | "@typescript-eslint/return-await": "error", 95 | "dot-notation": [ 96 | "error" 97 | ], 98 | "no-bitwise": [ 99 | "error" 100 | ], 101 | "@typescript-eslint/member-ordering": [ 102 | "error", 103 | { 104 | "default": [ 105 | "public-static-field", 106 | "public-static-method", 107 | "protected-static-field", 108 | "protected-static-method", 109 | "private-static-field", 110 | "private-static-method", 111 | "field", 112 | "constructor", 113 | "method" 114 | ] 115 | } 116 | ], 117 | "prettier/prettier": [ 118 | "error", 119 | { 120 | "singleQuote": true, 121 | "semi": true, 122 | "trailingComma": "es5" 123 | } 124 | ] 125 | }, 126 | "overrides": [ 127 | { 128 | "files": [ 129 | ".projenrc.ts" 130 | ], 131 | "rules": { 132 | "@typescript-eslint/no-require-imports": "off", 133 | "import/no-extraneous-dependencies": "off" 134 | } 135 | } 136 | ] 137 | } 138 | -------------------------------------------------------------------------------- /.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/build.yml linguist-generated 10 | /.github/workflows/pull-request-lint.yml linguist-generated 11 | /.github/workflows/release.yml linguist-generated 12 | /.github/workflows/upgrade-cdklabs-projen-project-types-main.yml linguist-generated 13 | /.github/workflows/upgrade-dev-deps-main.yml linguist-generated 14 | /.github/workflows/upgrade-main.yml linguist-generated 15 | /.gitignore linguist-generated 16 | /.mergify.yml linguist-generated 17 | /.npmignore linguist-generated 18 | /.projen/** linguist-generated 19 | /.projen/deps.json linguist-generated 20 | /.projen/files.json linguist-generated 21 | /.projen/tasks.json linguist-generated 22 | /API.md linguist-generated 23 | /LICENSE linguist-generated 24 | /package.json linguist-generated 25 | /test/integ/tsconfig.json linguist-generated 26 | /tsconfig.dev.json linguist-generated 27 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a bug 3 | title: "bug: short issue description" 4 | labels: [bug, needs-triage] 5 | body: 6 | - type: textarea 7 | id: problem 8 | attributes: 9 | label: What is the problem? 10 | validations: 11 | required: true 12 | 13 | - type: textarea 14 | id: reproduction 15 | attributes: 16 | label: Reproduction Steps 17 | description: | 18 | Minimal amount of code that causes the bug (if possible) or a reference. 19 | 20 | The code sample should be an SSCCE. See http://sscce.org/ for details. 21 | In short, provide a code sample that we can copy/paste, run and reproduce. 22 | validations: 23 | required: true 24 | 25 | - type: textarea 26 | id: expected 27 | attributes: 28 | label: What did you expect to happen? 29 | description: | 30 | What were you trying to achieve by performing the steps above? 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | id: actual 36 | attributes: 37 | label: What actually happened? 38 | description: | 39 | What is the unexpected behavior you were seeing? If you got an error, paste it here. 40 | validations: 41 | required: true 42 | 43 | - type: input 44 | id: cdk-enterprise-iac-version 45 | attributes: 46 | label: cdk-enterprise-iac version 47 | description: What version of cdk-enterprise-iac are you using? 48 | validations: 49 | required: true 50 | 51 | - type: dropdown 52 | id: language 53 | attributes: 54 | label: Language 55 | multiple: true 56 | options: 57 | - Typescript 58 | - Python 59 | - .NET 60 | - Java 61 | validations: 62 | required: true 63 | 64 | - type: textarea 65 | id: other 66 | attributes: 67 | label: Other information 68 | description: | 69 | e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. associated pull-request, stackoverflow, slack, etc 70 | validations: 71 | required: false 72 | 73 | - type: markdown 74 | attributes: 75 | value: | 76 | --- 77 | 78 | This is :bug: Bug Report 79 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/doc.yml: -------------------------------------------------------------------------------- 1 | name: Documentation Issue 2 | description: Issue in the reference documentation or developer guide 3 | title: 'doc: short issue description' 4 | labels: [documentation, needs-triage] 5 | body: 6 | - type: textarea 7 | id: issue 8 | attributes: 9 | label: Describe your issue? 10 | validations: 11 | required: true 12 | 13 | - type: markdown 14 | attributes: 15 | value: | 16 | --- 17 | 18 | This is a 📕 documentation issue 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Request a new feature 3 | title: "feat: short issue description" 4 | labels: [feature-request, needs-triage] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Short description of the feature you are proposing. 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: use-case 16 | attributes: 17 | label: Use Case 18 | description: | 19 | Why do you need this feature? 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: solution 25 | attributes: 26 | label: Proposed Solution 27 | description: | 28 | Please include prototype/workaround/sketch/reference implementation. 29 | validations: 30 | required: true 31 | 32 | - type: textarea 33 | id: other 34 | attributes: 35 | label: Other information 36 | description: | 37 | e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. associated pull-request, stackoverflow, slack, etc 38 | validations: 39 | required: false 40 | 41 | - type: checkboxes 42 | id: acknowledgments 43 | attributes: 44 | label: Acknowledge 45 | options: 46 | - label: I may be able to implement this feature request 47 | required: false 48 | - label: This feature might incur a breaking change 49 | required: false 50 | 51 | - type: markdown 52 | attributes: 53 | value: | 54 | --- 55 | 56 | This is a :rocket: Feature Request 57 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general-issue.yml: -------------------------------------------------------------------------------- 1 | name: General Issue 2 | description: Create a new issue 3 | title: "short issue description" 4 | labels: [needs-triage, guidance] 5 | body: 6 | 7 | - type: input 8 | id: issue 9 | attributes: 10 | label: General Issue 11 | description: | 12 | For support questions, please first reference our [documentation](https://github.com/cdklabs/cdk-enterprise-iac). 13 | validations: 14 | required: true 15 | 16 | - type: textarea 17 | id: question 18 | attributes: 19 | label: The Question 20 | description: | 21 | Ask your question here. Include any details relevant. Make sure you are not falling prey to the [X/Y problem](http://xyproblem.info)! 22 | validations: 23 | required: true 24 | 25 | - type: input 26 | id: cdk-enterprise-iac-version 27 | attributes: 28 | label: cdk-enterprise-iac version 29 | description: What version of cdk-enterprise-iac are you using? 30 | validations: 31 | required: true 32 | 33 | - type: dropdown 34 | id: language 35 | attributes: 36 | label: Language 37 | multiple: true 38 | options: 39 | - Typescript 40 | - Python 41 | - .NET 42 | - Java 43 | - Go 44 | validations: 45 | required: true 46 | 47 | - type: textarea 48 | id: other 49 | attributes: 50 | label: Other information 51 | description: | 52 | e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. associated pull-request, stackoverflow, slack, etc 53 | validations: 54 | required: false 55 | -------------------------------------------------------------------------------- /.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/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 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: lts/* 26 | - name: Install dependencies 27 | run: yarn install --check-files 28 | - name: build 29 | run: npx projen build 30 | - name: Find mutations 31 | id: self_mutation 32 | run: |- 33 | git add . 34 | git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 35 | working-directory: ./ 36 | - name: Upload patch 37 | if: steps.self_mutation.outputs.self_mutation_happened 38 | uses: actions/upload-artifact@v4.4.0 39 | with: 40 | name: repo.patch 41 | path: repo.patch 42 | overwrite: true 43 | - name: Fail build on mutation 44 | if: steps.self_mutation.outputs.self_mutation_happened 45 | run: |- 46 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 47 | cat repo.patch 48 | exit 1 49 | - name: Backup artifact permissions 50 | run: cd dist && getfacl -R . > permissions-backup.acl 51 | continue-on-error: true 52 | - name: Upload artifact 53 | uses: actions/upload-artifact@v4.4.0 54 | with: 55 | name: build-artifact 56 | path: dist 57 | overwrite: true 58 | self-mutation: 59 | needs: build 60 | runs-on: ubuntu-latest 61 | permissions: 62 | contents: write 63 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v4 67 | with: 68 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 69 | ref: ${{ github.event.pull_request.head.ref }} 70 | repository: ${{ github.event.pull_request.head.repo.full_name }} 71 | - name: Download patch 72 | uses: actions/download-artifact@v4 73 | with: 74 | name: repo.patch 75 | path: ${{ runner.temp }} 76 | - name: Apply patch 77 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 78 | - name: Set git identity 79 | run: |- 80 | git config user.name "github-actions" 81 | git config user.email "github-actions@github.com" 82 | - name: Push changes 83 | env: 84 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 85 | run: |- 86 | git add . 87 | git commit -s -m "chore: self mutation" 88 | git push origin HEAD:$PULL_REQUEST_REF 89 | package-js: 90 | needs: build 91 | runs-on: ubuntu-latest 92 | permissions: 93 | contents: read 94 | if: ${{ !needs.build.outputs.self_mutation_happened }} 95 | steps: 96 | - uses: actions/setup-node@v4 97 | with: 98 | node-version: lts/* 99 | - name: Download build artifacts 100 | uses: actions/download-artifact@v4 101 | with: 102 | name: build-artifact 103 | path: dist 104 | - name: Restore build artifact permissions 105 | run: cd dist && setfacl --restore=permissions-backup.acl 106 | continue-on-error: true 107 | - name: Checkout 108 | uses: actions/checkout@v4 109 | with: 110 | ref: ${{ github.event.pull_request.head.ref }} 111 | repository: ${{ github.event.pull_request.head.repo.full_name }} 112 | path: .repo 113 | - name: Install Dependencies 114 | run: cd .repo && yarn install --check-files --frozen-lockfile 115 | - name: Extract build artifact 116 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 117 | - name: Move build artifact out of the way 118 | run: mv dist dist.old 119 | - name: Create js artifact 120 | run: cd .repo && npx projen package:js 121 | - name: Collect js artifact 122 | run: mv .repo/dist dist 123 | package-java: 124 | needs: build 125 | runs-on: ubuntu-latest 126 | permissions: 127 | contents: read 128 | if: ${{ !needs.build.outputs.self_mutation_happened }} 129 | steps: 130 | - uses: actions/setup-java@v4 131 | with: 132 | distribution: corretto 133 | java-version: "11" 134 | - uses: actions/setup-node@v4 135 | with: 136 | node-version: lts/* 137 | - name: Download build artifacts 138 | uses: actions/download-artifact@v4 139 | with: 140 | name: build-artifact 141 | path: dist 142 | - name: Restore build artifact permissions 143 | run: cd dist && setfacl --restore=permissions-backup.acl 144 | continue-on-error: true 145 | - name: Checkout 146 | uses: actions/checkout@v4 147 | with: 148 | ref: ${{ github.event.pull_request.head.ref }} 149 | repository: ${{ github.event.pull_request.head.repo.full_name }} 150 | path: .repo 151 | - name: Install Dependencies 152 | run: cd .repo && yarn install --check-files --frozen-lockfile 153 | - name: Extract build artifact 154 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 155 | - name: Move build artifact out of the way 156 | run: mv dist dist.old 157 | - name: Create java artifact 158 | run: cd .repo && npx projen package:java 159 | - name: Collect java artifact 160 | run: mv .repo/dist dist 161 | package-python: 162 | needs: build 163 | runs-on: ubuntu-latest 164 | permissions: 165 | contents: read 166 | if: ${{ !needs.build.outputs.self_mutation_happened }} 167 | steps: 168 | - uses: actions/setup-node@v4 169 | with: 170 | node-version: lts/* 171 | - uses: actions/setup-python@v5 172 | with: 173 | python-version: 3.x 174 | - name: Download build artifacts 175 | uses: actions/download-artifact@v4 176 | with: 177 | name: build-artifact 178 | path: dist 179 | - name: Restore build artifact permissions 180 | run: cd dist && setfacl --restore=permissions-backup.acl 181 | continue-on-error: true 182 | - name: Checkout 183 | uses: actions/checkout@v4 184 | with: 185 | ref: ${{ github.event.pull_request.head.ref }} 186 | repository: ${{ github.event.pull_request.head.repo.full_name }} 187 | path: .repo 188 | - name: Install Dependencies 189 | run: cd .repo && yarn install --check-files --frozen-lockfile 190 | - name: Extract build artifact 191 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 192 | - name: Move build artifact out of the way 193 | run: mv dist dist.old 194 | - name: Create python artifact 195 | run: cd .repo && npx projen package:python 196 | - name: Collect python artifact 197 | run: mv .repo/dist dist 198 | package-dotnet: 199 | needs: build 200 | runs-on: ubuntu-latest 201 | permissions: 202 | contents: read 203 | if: ${{ !needs.build.outputs.self_mutation_happened }} 204 | steps: 205 | - uses: actions/setup-node@v4 206 | with: 207 | node-version: lts/* 208 | - uses: actions/setup-dotnet@v4 209 | with: 210 | dotnet-version: 6.x 211 | - name: Download build artifacts 212 | uses: actions/download-artifact@v4 213 | with: 214 | name: build-artifact 215 | path: dist 216 | - name: Restore build artifact permissions 217 | run: cd dist && setfacl --restore=permissions-backup.acl 218 | continue-on-error: true 219 | - name: Checkout 220 | uses: actions/checkout@v4 221 | with: 222 | ref: ${{ github.event.pull_request.head.ref }} 223 | repository: ${{ github.event.pull_request.head.repo.full_name }} 224 | path: .repo 225 | - name: Install Dependencies 226 | run: cd .repo && yarn install --check-files --frozen-lockfile 227 | - name: Extract build artifact 228 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 229 | - name: Move build artifact out of the way 230 | run: mv dist dist.old 231 | - name: Create dotnet artifact 232 | run: cd .repo && npx projen package:dotnet 233 | - name: Collect dotnet artifact 234 | run: mv .repo/dist dist 235 | -------------------------------------------------------------------------------- /.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 | runs-on: ubuntu-latest 70 | permissions: 71 | contents: write 72 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 73 | steps: 74 | - uses: actions/setup-node@v4 75 | with: 76 | node-version: lts/* 77 | - name: Download build artifacts 78 | uses: actions/download-artifact@v4 79 | with: 80 | name: build-artifact 81 | path: dist 82 | - name: Restore build artifact permissions 83 | run: cd dist && setfacl --restore=permissions-backup.acl 84 | continue-on-error: true 85 | - name: Release 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 88 | 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 89 | release_npm: 90 | name: Publish to npm 91 | needs: release 92 | runs-on: ubuntu-latest 93 | permissions: 94 | id-token: write 95 | contents: read 96 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 97 | steps: 98 | - uses: actions/setup-node@v4 99 | with: 100 | node-version: lts/* 101 | - name: Download build artifacts 102 | uses: actions/download-artifact@v4 103 | with: 104 | name: build-artifact 105 | path: dist 106 | - name: Restore build artifact permissions 107 | run: cd dist && setfacl --restore=permissions-backup.acl 108 | continue-on-error: true 109 | - name: Checkout 110 | uses: actions/checkout@v4 111 | with: 112 | path: .repo 113 | - name: Install Dependencies 114 | run: cd .repo && yarn install --check-files --frozen-lockfile 115 | - name: Extract build artifact 116 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 117 | - name: Move build artifact out of the way 118 | run: mv dist dist.old 119 | - name: Create js artifact 120 | run: cd .repo && npx projen package:js 121 | - name: Collect js artifact 122 | run: mv .repo/dist dist 123 | - name: Release 124 | env: 125 | NPM_DIST_TAG: latest 126 | NPM_REGISTRY: registry.npmjs.org 127 | NPM_CONFIG_PROVENANCE: "true" 128 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 129 | run: npx -p publib@latest publib-npm 130 | release_maven: 131 | name: Publish to Maven Central 132 | needs: release 133 | runs-on: ubuntu-latest 134 | permissions: 135 | contents: read 136 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 137 | steps: 138 | - uses: actions/setup-java@v4 139 | with: 140 | distribution: corretto 141 | java-version: "11" 142 | - uses: actions/setup-node@v4 143 | with: 144 | node-version: lts/* 145 | - name: Download build artifacts 146 | uses: actions/download-artifact@v4 147 | with: 148 | name: build-artifact 149 | path: dist 150 | - name: Restore build artifact permissions 151 | run: cd dist && setfacl --restore=permissions-backup.acl 152 | continue-on-error: true 153 | - name: Checkout 154 | uses: actions/checkout@v4 155 | with: 156 | path: .repo 157 | - name: Install Dependencies 158 | run: cd .repo && yarn install --check-files --frozen-lockfile 159 | - name: Extract build artifact 160 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 161 | - name: Move build artifact out of the way 162 | run: mv dist dist.old 163 | - name: Create java artifact 164 | run: cd .repo && npx projen package:java 165 | - name: Collect java artifact 166 | run: mv .repo/dist dist 167 | - name: Release 168 | env: 169 | MAVEN_SERVER_ID: central-ossrh 170 | MAVEN_GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} 171 | MAVEN_GPG_PRIVATE_KEY_PASSPHRASE: ${{ secrets.MAVEN_GPG_PRIVATE_KEY_PASSPHRASE }} 172 | MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }} 173 | MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} 174 | MAVEN_STAGING_PROFILE_ID: ${{ secrets.MAVEN_STAGING_PROFILE_ID }} 175 | run: npx -p publib@latest publib-maven 176 | release_pypi: 177 | name: Publish to PyPI 178 | needs: release 179 | runs-on: ubuntu-latest 180 | permissions: 181 | contents: read 182 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 183 | steps: 184 | - uses: actions/setup-node@v4 185 | with: 186 | node-version: lts/* 187 | - uses: actions/setup-python@v5 188 | with: 189 | python-version: 3.x 190 | - name: Download build artifacts 191 | uses: actions/download-artifact@v4 192 | with: 193 | name: build-artifact 194 | path: dist 195 | - name: Restore build artifact permissions 196 | run: cd dist && setfacl --restore=permissions-backup.acl 197 | continue-on-error: true 198 | - name: Checkout 199 | uses: actions/checkout@v4 200 | with: 201 | path: .repo 202 | - name: Install Dependencies 203 | run: cd .repo && yarn install --check-files --frozen-lockfile 204 | - name: Extract build artifact 205 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 206 | - name: Move build artifact out of the way 207 | run: mv dist dist.old 208 | - name: Create python artifact 209 | run: cd .repo && npx projen package:python 210 | - name: Collect python artifact 211 | run: mv .repo/dist dist 212 | - name: Release 213 | env: 214 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} 215 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} 216 | run: npx -p publib@latest publib-pypi 217 | release_nuget: 218 | name: Publish to NuGet Gallery 219 | needs: release 220 | runs-on: ubuntu-latest 221 | permissions: 222 | contents: read 223 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 224 | steps: 225 | - uses: actions/setup-node@v4 226 | with: 227 | node-version: lts/* 228 | - uses: actions/setup-dotnet@v4 229 | with: 230 | dotnet-version: 6.x 231 | - name: Download build artifacts 232 | uses: actions/download-artifact@v4 233 | with: 234 | name: build-artifact 235 | path: dist 236 | - name: Restore build artifact permissions 237 | run: cd dist && setfacl --restore=permissions-backup.acl 238 | continue-on-error: true 239 | - name: Checkout 240 | uses: actions/checkout@v4 241 | with: 242 | path: .repo 243 | - name: Install Dependencies 244 | run: cd .repo && yarn install --check-files --frozen-lockfile 245 | - name: Extract build artifact 246 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 247 | - name: Move build artifact out of the way 248 | run: mv dist dist.old 249 | - name: Create dotnet artifact 250 | run: cd .repo && npx projen package:dotnet 251 | - name: Collect dotnet artifact 252 | run: mv .repo/dist dist 253 | - name: Release 254 | env: 255 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 256 | run: npx -p publib@latest publib-nuget 257 | -------------------------------------------------------------------------------- /.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 | .vscode/ 35 | *.d.ts 36 | *.generated.ts 37 | *.js 38 | *.js.map 39 | !**/*.integ.snapshot/**/asset.*/*.js 40 | !**/*.integ.snapshot/**/asset.*/*.d.ts 41 | !**/*.integ.snapshot/**/asset.*/** 42 | *.bak 43 | /test-reports/ 44 | junit.xml 45 | /coverage/ 46 | !/.github/workflows/build.yml 47 | /dist/changelog.md 48 | /dist/version.txt 49 | !/.github/workflows/release.yml 50 | !/.mergify.yml 51 | !/.github/pull_request_template.md 52 | !/test/ 53 | !/tsconfig.dev.json 54 | !/src/ 55 | /lib 56 | /dist/ 57 | !/.eslintrc.json 58 | .jsii 59 | tsconfig.json 60 | !/API.md 61 | .jsii.tabl.json 62 | !/rosetta/default.ts-fixture 63 | !/.github/workflows/upgrade-cdklabs-projen-project-types-main.yml 64 | !/.github/workflows/upgrade-main.yml 65 | !/.github/workflows/upgrade-dev-deps-main.yml 66 | !/test/integ/tsconfig.json 67 | !/.projenrc.ts 68 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | update_method: merge 6 | conditions: 7 | - "#approved-reviews-by>=1" 8 | - -label~=(do-not-merge) 9 | - status-success=build 10 | - status-success=package-js 11 | - status-success=package-java 12 | - status-success=package-python 13 | - status-success=package-dotnet 14 | merge_method: squash 15 | commit_message_template: |- 16 | {{ title }} (#{{ number }}) 17 | 18 | {{ body }} 19 | pull_request_rules: 20 | - name: Automatic merge on approval and successful build 21 | actions: 22 | delete_head_branch: {} 23 | queue: 24 | name: default 25 | conditions: 26 | - "#approved-reviews-by>=1" 27 | - -label~=(do-not-merge) 28 | - status-success=build 29 | - status-success=package-js 30 | - status-success=package-java 31 | - status-success=package-python 32 | - status-success=package-dotnet 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /dist/changelog.md 8 | /dist/version.txt 9 | /.mergify.yml 10 | /test/ 11 | /tsconfig.dev.json 12 | /src/ 13 | !/lib/ 14 | !/lib/**/*.js 15 | !/lib/**/*.d.ts 16 | dist 17 | /tsconfig.json 18 | /.github/ 19 | /.vscode/ 20 | /.idea/ 21 | /.projenrc.js 22 | tsconfig.tsbuildinfo 23 | /.eslintrc.json 24 | !.jsii 25 | /.gitattributes 26 | /.projenrc.ts 27 | /projenrc 28 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | default_language_version: 4 | node: system 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.3.0 8 | hooks: 9 | - id: check-json 10 | exclude: | 11 | (?x)( 12 | ^.eslintrc.json| 13 | tsconfig* 14 | ) 15 | - id: trailing-whitespace 16 | exclude: ^API.md||.github/ 17 | 18 | - repo: https://github.com/pre-commit/mirrors-eslint 19 | rev: v8.21.0 20 | hooks: 21 | - id: eslint 22 | files: \.[jt]sx?$ 23 | types: [file] 24 | - repo: https://github.com/pre-commit/mirrors-prettier 25 | rev: 'v2.7.1' 26 | hooks: 27 | - id: prettier 28 | exclude: | 29 | (?x)( 30 | ^.github/| 31 | ^.projen/| 32 | ^.mergify.yml| 33 | ^.*.json | 34 | ^API.md | 35 | ^README.md 36 | ) 37 | - repo: https://github.com/dontirun/text-prepender 38 | rev: v0.1.0 39 | hooks: 40 | - id: text-prepender 41 | exclude: | 42 | (?x)( 43 | ^.github/| 44 | ^.projen/| 45 | ^.mergify.yml| 46 | ^.*.json | 47 | ^API.md 48 | ) 49 | - repo: local 50 | hooks: 51 | - id: append-api-to-readme 52 | name: append-api-to-readme 53 | entry: scripts/append-api-docs-to-readme.sh 54 | language: script 55 | pass_filenames: false 56 | files: API.md 57 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@aws-sdk/client-cloudformation", 5 | "version": "3.600.0", 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-config-prettier", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "eslint-import-resolver-typescript", 42 | "type": "build" 43 | }, 44 | { 45 | "name": "eslint-plugin-import", 46 | "type": "build" 47 | }, 48 | { 49 | "name": "eslint-plugin-prettier", 50 | "type": "build" 51 | }, 52 | { 53 | "name": "eslint-plugin-security", 54 | "type": "build" 55 | }, 56 | { 57 | "name": "eslint", 58 | "version": "^9", 59 | "type": "build" 60 | }, 61 | { 62 | "name": "jest", 63 | "type": "build" 64 | }, 65 | { 66 | "name": "jest-junit", 67 | "version": "^16", 68 | "type": "build" 69 | }, 70 | { 71 | "name": "jsii-diff", 72 | "type": "build" 73 | }, 74 | { 75 | "name": "jsii-docgen", 76 | "version": "^10.5.0", 77 | "type": "build" 78 | }, 79 | { 80 | "name": "jsii-pacmak", 81 | "type": "build" 82 | }, 83 | { 84 | "name": "jsii-rosetta", 85 | "type": "build" 86 | }, 87 | { 88 | "name": "jsii", 89 | "version": "5.5.x", 90 | "type": "build" 91 | }, 92 | { 93 | "name": "natural-compare-lite", 94 | "type": "build" 95 | }, 96 | { 97 | "name": "prettier", 98 | "type": "build" 99 | }, 100 | { 101 | "name": "projen", 102 | "type": "build" 103 | }, 104 | { 105 | "name": "ts-jest", 106 | "type": "build" 107 | }, 108 | { 109 | "name": "ts-node", 110 | "type": "build" 111 | }, 112 | { 113 | "name": "typescript", 114 | "version": "5.5.x", 115 | "type": "build" 116 | }, 117 | { 118 | "name": "@aws-cdk/integ-runner", 119 | "version": "latest", 120 | "type": "devenv" 121 | }, 122 | { 123 | "name": "@aws-cdk/integ-tests-alpha", 124 | "version": "latest", 125 | "type": "devenv" 126 | }, 127 | { 128 | "name": "aws-cdk-lib", 129 | "version": "^2.103.1", 130 | "type": "peer" 131 | }, 132 | { 133 | "name": "constructs", 134 | "version": "^10.0.5", 135 | "type": "peer" 136 | }, 137 | { 138 | "name": "@aws-cdk/integ-tests-alpha", 139 | "version": "2.103.1-alpha.0", 140 | "type": "test" 141 | } 142 | ], 143 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 144 | } 145 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/auto-approve.yml", 7 | ".github/workflows/build.yml", 8 | ".github/workflows/pull-request-lint.yml", 9 | ".github/workflows/release.yml", 10 | ".github/workflows/upgrade-cdklabs-projen-project-types-main.yml", 11 | ".github/workflows/upgrade-dev-deps-main.yml", 12 | ".github/workflows/upgrade-main.yml", 13 | ".gitignore", 14 | ".mergify.yml", 15 | ".projen/deps.json", 16 | ".projen/files.json", 17 | ".projen/tasks.json", 18 | "LICENSE", 19 | "test/integ/tsconfig.json", 20 | "tsconfig.dev.json" 21 | ], 22 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 23 | } 24 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "", 36 | "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 projenrc test build-tools .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 | }, 209 | "package:dotnet": { 210 | "name": "package:dotnet", 211 | "description": "Create dotnet language bindings", 212 | "steps": [ 213 | { 214 | "exec": "jsii-pacmak -v --target dotnet" 215 | } 216 | ] 217 | }, 218 | "package:java": { 219 | "name": "package:java", 220 | "description": "Create java language bindings", 221 | "steps": [ 222 | { 223 | "exec": "jsii-pacmak -v --target java" 224 | } 225 | ] 226 | }, 227 | "package:js": { 228 | "name": "package:js", 229 | "description": "Create js language bindings", 230 | "steps": [ 231 | { 232 | "exec": "jsii-pacmak -v --target js" 233 | } 234 | ] 235 | }, 236 | "package:python": { 237 | "name": "package:python", 238 | "description": "Create python language bindings", 239 | "steps": [ 240 | { 241 | "exec": "jsii-pacmak -v --target python" 242 | } 243 | ] 244 | }, 245 | "post-compile": { 246 | "name": "post-compile", 247 | "description": "Runs after successful compilation", 248 | "steps": [ 249 | { 250 | "spawn": "docgen" 251 | }, 252 | { 253 | "spawn": "rosetta:extract" 254 | } 255 | ] 256 | }, 257 | "post-upgrade": { 258 | "name": "post-upgrade", 259 | "description": "Runs after upgrading dependencies" 260 | }, 261 | "pre-compile": { 262 | "name": "pre-compile", 263 | "description": "Prepare the project for compilation" 264 | }, 265 | "release": { 266 | "name": "release", 267 | "description": "Prepare a release from \"main\" branch", 268 | "env": { 269 | "RELEASE": "true" 270 | }, 271 | "steps": [ 272 | { 273 | "exec": "rm -fr dist" 274 | }, 275 | { 276 | "spawn": "bump" 277 | }, 278 | { 279 | "spawn": "build" 280 | }, 281 | { 282 | "spawn": "unbump" 283 | }, 284 | { 285 | "exec": "git diff --ignore-space-at-eol --exit-code" 286 | } 287 | ] 288 | }, 289 | "rosetta:extract": { 290 | "name": "rosetta:extract", 291 | "description": "Test rosetta extract", 292 | "steps": [ 293 | { 294 | "exec": "yarn --silent jsii-rosetta extract" 295 | } 296 | ] 297 | }, 298 | "test": { 299 | "name": "test", 300 | "description": "Run tests", 301 | "steps": [ 302 | { 303 | "exec": "jest --passWithNoTests --updateSnapshot", 304 | "receiveArgs": true 305 | }, 306 | { 307 | "spawn": "eslint" 308 | }, 309 | { 310 | "spawn": "integ" 311 | } 312 | ] 313 | }, 314 | "test:watch": { 315 | "name": "test:watch", 316 | "description": "Run jest in watch mode", 317 | "steps": [ 318 | { 319 | "exec": "jest --watch" 320 | } 321 | ] 322 | }, 323 | "unbump": { 324 | "name": "unbump", 325 | "description": "Restores version to 0.0.0", 326 | "env": { 327 | "OUTFILE": "package.json", 328 | "CHANGELOG": "dist/changelog.md", 329 | "BUMPFILE": "dist/version.txt", 330 | "RELEASETAG": "dist/releasetag.txt", 331 | "RELEASE_TAG_PREFIX": "", 332 | "BUMP_PACKAGE": "commit-and-tag-version@^12", 333 | "RELEASABLE_COMMITS": "git log --no-merges --oneline $LATEST_TAG..HEAD -E --grep \"^(feat|fix){1}(\\([^()[:space:]]+\\))?(!)?:[[:blank:]]+.+\"" 334 | }, 335 | "steps": [ 336 | { 337 | "builtin": "release/reset-version" 338 | } 339 | ] 340 | }, 341 | "upgrade": { 342 | "name": "upgrade", 343 | "description": "upgrade dependencies", 344 | "env": { 345 | "CI": "0" 346 | }, 347 | "steps": [ 348 | { 349 | "exec": "echo No dependencies to upgrade." 350 | } 351 | ] 352 | }, 353 | "upgrade-cdklabs-projen-project-types": { 354 | "name": "upgrade-cdklabs-projen-project-types", 355 | "description": "upgrade cdklabs-projen-project-types", 356 | "env": { 357 | "CI": "0" 358 | }, 359 | "steps": [ 360 | { 361 | "exec": "npx npm-check-updates@16 --upgrade --target=latest --peer --no-deprecated --dep=dev,peer,prod,optional --filter=cdklabs-projen-project-types,projen" 362 | }, 363 | { 364 | "exec": "yarn install --check-files" 365 | }, 366 | { 367 | "exec": "yarn upgrade cdklabs-projen-project-types projen" 368 | }, 369 | { 370 | "exec": "npx projen" 371 | }, 372 | { 373 | "spawn": "post-upgrade" 374 | } 375 | ] 376 | }, 377 | "upgrade-dev-deps": { 378 | "name": "upgrade-dev-deps", 379 | "description": "upgrade dev dependencies", 380 | "env": { 381 | "CI": "0" 382 | }, 383 | "steps": [ 384 | { 385 | "exec": "npx npm-check-updates@16 --upgrade --target=minor --peer --no-deprecated --dep=dev --filter=@types/jest,eslint-config-prettier,eslint-import-resolver-typescript,eslint-plugin-import,eslint-plugin-prettier,eslint-plugin-security,jest,jsii-diff,jsii-pacmak,jsii-rosetta,natural-compare-lite,prettier,ts-jest,ts-node" 386 | }, 387 | { 388 | "exec": "yarn install --check-files" 389 | }, 390 | { 391 | "exec": "yarn upgrade @aws-sdk/client-cloudformation @types/jest @types/node @typescript-eslint/eslint-plugin @typescript-eslint/parser commit-and-tag-version eslint-config-prettier eslint-import-resolver-typescript eslint-plugin-import eslint-plugin-prettier eslint-plugin-security eslint jest jest-junit jsii-diff jsii-docgen jsii-pacmak jsii-rosetta jsii natural-compare-lite prettier ts-jest ts-node typescript @aws-cdk/integ-runner @aws-cdk/integ-tests-alpha" 392 | }, 393 | { 394 | "exec": "npx projen" 395 | }, 396 | { 397 | "spawn": "post-upgrade" 398 | } 399 | ] 400 | }, 401 | "watch": { 402 | "name": "watch", 403 | "description": "Watch & compile in the background", 404 | "steps": [ 405 | { 406 | "exec": "jsii -w --silence-warnings=reserved-word" 407 | } 408 | ] 409 | } 410 | }, 411 | "env": { 412 | "PATH": "$(npx -c \"node --print process.env.PATH\")" 413 | }, 414 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 415 | } 416 | -------------------------------------------------------------------------------- /.projenrc.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { 6 | CdklabsConstructLibrary, 7 | JsiiLanguage, 8 | } from 'cdklabs-projen-project-types'; 9 | import { DependencyType, JsonFile } from 'projen'; 10 | import { NodePackageManager } from 'projen/lib/javascript'; 11 | 12 | const project = new CdklabsConstructLibrary({ 13 | setNodeEngineVersion: false, 14 | stability: 'experimental', 15 | private: false, 16 | author: 'Taylor Ondrey', 17 | authorAddress: 'ondreyt@amazon.com', 18 | projenrcTs: true, 19 | cdkVersion: '2.103.1', 20 | rosettaOptions: { 21 | strict: false, 22 | }, 23 | defaultReleaseBranch: 'main', 24 | name: '@cdklabs/cdk-enterprise-iac', 25 | repositoryUrl: 'https://github.com/cdklabs/cdk-enterprise-iac.git', 26 | devDeps: [ 27 | 'eslint-plugin-security', 28 | 'natural-compare-lite', 29 | '@aws-sdk/client-cloudformation@3.600.0', 30 | ], 31 | jsiiVersion: '5.5.x', 32 | typescriptVersion: '5.5.x', 33 | gitignore: [ 34 | '.vscode/', 35 | '*.d.ts', 36 | '*.generated.ts', 37 | '*.js', 38 | '*.js.map', 39 | '!**/*.integ.snapshot/**/asset.*/*.js', 40 | '!**/*.integ.snapshot/**/asset.*/*.d.ts', 41 | '!**/*.integ.snapshot/**/asset.*/**', 42 | '*.bak', 43 | ], 44 | eslintOptions: { prettier: true, dirs: ['src', 'projenrc'] }, 45 | jsiiTargetLanguages: [ 46 | JsiiLanguage.PYTHON, 47 | JsiiLanguage.DOTNET, 48 | JsiiLanguage.JAVA, 49 | ], 50 | publishToPypi: { 51 | distName: 'cdklabs.cdk-enterprise-iac', 52 | module: 'cdklabs.cdk_enterprise_iac', 53 | }, 54 | publishToNuget: { 55 | packageId: 'Cdklabs.CdkEnterpriseIac', 56 | dotNetNamespace: 'Cdklabs.CdkEnterpriseIac', 57 | }, 58 | publishToMaven: { 59 | mavenGroupId: 'io.github.cdklabs', 60 | javaPackage: 'io.github.cdklabs.cdkenterpriseiac', 61 | mavenArtifactId: 'cdkenterpriseiac', 62 | mavenServerId: 'central-ossrh', 63 | }, 64 | release: true, 65 | packageManager: NodePackageManager.YARN_CLASSIC, 66 | }); 67 | 68 | project.addFields({ 69 | packageManager: 'yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447', 70 | }); 71 | 72 | project.package.addField('prettier', { 73 | singleQuote: true, 74 | semi: true, 75 | trailingComma: 'es5', 76 | }); 77 | project.eslint?.addRules({ 78 | 'prettier/prettier': [ 79 | 'error', 80 | { singleQuote: true, semi: true, trailingComma: 'es5' }, 81 | ], 82 | }); 83 | project.eslint?.addExtends('plugin:security/recommended-legacy'); 84 | 85 | project.deps.addDependency( 86 | '@aws-cdk/integ-tests-alpha@2.103.1-alpha.0', 87 | DependencyType.TEST 88 | ); 89 | new JsonFile(project, 'test/integ/tsconfig.json', { 90 | obj: { 91 | extends: '../../tsconfig.dev.json', 92 | include: ['./**/integ.*.ts'], 93 | }, 94 | }); 95 | project.synth(); 96 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ## Code of Conduct 7 | 8 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 9 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 10 | opensource-codeofconduct@amazon.com with any additional questions or comments. 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | # Contributing Guidelines 7 | 8 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 9 | documentation, we greatly value feedback and contributions from our community. 10 | 11 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 12 | information to effectively respond to your bug report or contribution. 13 | 14 | ## Reporting Bugs/Feature Requests 15 | 16 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 17 | 18 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 19 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 20 | 21 | - A reproducible test case or series of steps 22 | - The version of our code being used 23 | - Any modifications you've made relevant to the bug 24 | - Anything unusual about your environment or deployment 25 | 26 | ## Contributing via Pull Requests 27 | 28 | ### Pull Request Checklist 29 | 30 | - [ ] Testing 31 | - Unit test added (prefer not to modify an existing test, otherwise, it's probably a breaking change) 32 | - [ ] Docs 33 | - **README**: README updated if necessary 34 | - [ ] Title and Description 35 | - **Change type**: title prefixed with **fix**, **feat** and module name in parens, which will appear in changelog 36 | - **Title**: use lower-case and doesn't end with a period 37 | - **Breaking?**: last paragraph: "BREAKING CHANGE: " 38 | - **Issues**: Indicate issues fixed via: "**Fixes #xxx**" or "**Closes #xxx**" 39 | 40 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 41 | 42 | 1. You are working against the latest source on the _main_ branch. 43 | 44 | --- 45 | 46 | ### Step 1: Open Issue 47 | 48 | If there isn't one already, open an issue describing what you intend to contribute. It's useful to communicate in advance, because sometimes, someone is already working in this space, so maybe it's worth collaborating with them instead of duplicating the efforts. 49 | 50 | ### Step 2: Fork the repository 51 | 52 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/). Make sure you are working against the latest source on the _main_ branch. 53 | 54 | ### Step 3: Setup 55 | 56 | The following tools need to be installed on your system prior to developing 57 | 58 | - [Node.js >= 14.15.0](https://nodejs.org/download/release/latest-v14.x/) 59 | - We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) 60 | - [Yarn >= 1.19.1, < 2](https://yarnpkg.com/lang/en/docs/install) 61 | - [Docker >= 19.03](https://docs.docker.com/get-docker/) 62 | - the Docker daemon must also be running 63 | 64 | ### Step 4: Develop 65 | 66 | This repository uses [pre-commit](https://pre-commit.com/) hooks for linting and [projen](https://github.com/projen/projen) for building and testing 67 | 68 | 1. Follow the [quickstart guide](https://pre-commit.com/#quick-start) for pre-commit (skipping step #2) 69 | 2. Install dependencies 70 | 71 | - `yarn install` 72 | - `npx projen` 73 | 74 | 3. Change code 75 | 4. If relevant, add [tests](./test/) 76 | 5. Run tests 77 | - `npx projen test` 78 | 6. Build 79 | - `npx projen build` 80 | 7. Update relevant documentation 81 | 8. Create the commit with relevant files 82 | - Note: you may need to update the commit if `pre-commit` changes/suggests changes to files 83 | 84 | ### Step 5: Make the pull request 85 | 86 | Send us a [pull request](https://help.github.com/articles/creating-a-pull-request/), answering any default questions in the pull request interface. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 87 | 88 | ## Finding contributions to work on 89 | 90 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 91 | 92 | ## Code of Conduct 93 | 94 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 95 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 96 | opensource-codeofconduct@amazon.com with any additional questions or comments. 97 | 98 | ## Security issue notifications 99 | 100 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 101 | 102 | ## Licensing 103 | 104 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | SPDX-License-Identifier: Apache-2.0 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@cdklabs/cdk-enterprise-iac", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/cdklabs/cdk-enterprise-iac.git" 6 | }, 7 | "scripts": { 8 | "build": "npx projen build", 9 | "bump": "npx projen bump", 10 | "clobber": "npx projen clobber", 11 | "compat": "npx projen compat", 12 | "compile": "npx projen compile", 13 | "default": "npx projen default", 14 | "docgen": "npx projen docgen", 15 | "eject": "npx projen eject", 16 | "eslint": "npx projen eslint", 17 | "integ": "npx projen integ", 18 | "integ:update": "npx projen integ:update", 19 | "package": "npx projen package", 20 | "package-all": "npx projen package-all", 21 | "package:dotnet": "npx projen package:dotnet", 22 | "package:java": "npx projen package:java", 23 | "package:js": "npx projen package:js", 24 | "package:python": "npx projen package:python", 25 | "post-compile": "npx projen post-compile", 26 | "post-upgrade": "npx projen post-upgrade", 27 | "pre-compile": "npx projen pre-compile", 28 | "release": "npx projen release", 29 | "rosetta:extract": "npx projen rosetta:extract", 30 | "test": "npx projen test", 31 | "test:watch": "npx projen test:watch", 32 | "unbump": "npx projen unbump", 33 | "upgrade": "npx projen upgrade", 34 | "upgrade-cdklabs-projen-project-types": "npx projen upgrade-cdklabs-projen-project-types", 35 | "upgrade-dev-deps": "npx projen upgrade-dev-deps", 36 | "watch": "npx projen watch", 37 | "projen": "npx projen" 38 | }, 39 | "author": { 40 | "name": "Amazon Web Services", 41 | "email": "aws-cdk-dev@amazon.com", 42 | "organization": true 43 | }, 44 | "devDependencies": { 45 | "@aws-cdk/integ-runner": "latest", 46 | "@aws-cdk/integ-tests-alpha": "2.103.1-alpha.0", 47 | "@aws-sdk/client-cloudformation": "3.600.0", 48 | "@types/jest": "^29", 49 | "@types/node": "^18", 50 | "@typescript-eslint/eslint-plugin": "^8", 51 | "@typescript-eslint/parser": "^8", 52 | "aws-cdk-lib": "2.103.1", 53 | "cdklabs-projen-project-types": "^0.3.1", 54 | "commit-and-tag-version": "^12", 55 | "constructs": "10.0.5", 56 | "eslint": "^9", 57 | "eslint-config-prettier": "^8.10.0", 58 | "eslint-import-resolver-typescript": "^3.10.1", 59 | "eslint-plugin-import": "^2.31.0", 60 | "eslint-plugin-prettier": "^4.2.1", 61 | "eslint-plugin-security": "^3.0.1", 62 | "jest": "^29", 63 | "jest-junit": "^16", 64 | "jsii": "5.5.x", 65 | "jsii-diff": "^1.112.0", 66 | "jsii-docgen": "^10.5.0", 67 | "jsii-pacmak": "^1.112.0", 68 | "jsii-rosetta": "^5.8.9", 69 | "natural-compare-lite": "^1.4.0", 70 | "prettier": "^2.8.8", 71 | "projen": "^0.92.9", 72 | "ts-jest": "^29", 73 | "ts-node": "^10.9.2", 74 | "typescript": "5.5.x" 75 | }, 76 | "peerDependencies": { 77 | "aws-cdk-lib": "^2.103.1", 78 | "constructs": "^10.0.5" 79 | }, 80 | "keywords": [ 81 | "cdk" 82 | ], 83 | "main": "lib/index.js", 84 | "license": "Apache-2.0", 85 | "publishConfig": { 86 | "access": "public" 87 | }, 88 | "version": "0.0.0", 89 | "jest": { 90 | "coverageProvider": "v8", 91 | "testMatch": [ 92 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 93 | "/@(src|test)/**/__tests__/**/*.ts?(x)", 94 | "/@(projenrc)/**/*(*.)@(spec|test).ts?(x)", 95 | "/@(projenrc)/**/__tests__/**/*.ts?(x)" 96 | ], 97 | "clearMocks": true, 98 | "collectCoverage": true, 99 | "coverageReporters": [ 100 | "json", 101 | "lcov", 102 | "clover", 103 | "cobertura", 104 | "text" 105 | ], 106 | "coverageDirectory": "coverage", 107 | "coveragePathIgnorePatterns": [ 108 | "/node_modules/" 109 | ], 110 | "testPathIgnorePatterns": [ 111 | "/node_modules/" 112 | ], 113 | "watchPathIgnorePatterns": [ 114 | "/node_modules/" 115 | ], 116 | "reporters": [ 117 | "default", 118 | [ 119 | "jest-junit", 120 | { 121 | "outputDirectory": "test-reports" 122 | } 123 | ] 124 | ], 125 | "transform": { 126 | "^.+\\.[t]sx?$": [ 127 | "ts-jest", 128 | { 129 | "tsconfig": "tsconfig.dev.json" 130 | } 131 | ] 132 | } 133 | }, 134 | "types": "lib/index.d.ts", 135 | "stability": "experimental", 136 | "jsii": { 137 | "outdir": "dist", 138 | "targets": { 139 | "java": { 140 | "package": "io.github.cdklabs.cdkenterpriseiac", 141 | "maven": { 142 | "groupId": "io.github.cdklabs", 143 | "artifactId": "cdkenterpriseiac" 144 | } 145 | }, 146 | "python": { 147 | "distName": "cdklabs.cdk-enterprise-iac", 148 | "module": "cdklabs.cdk_enterprise_iac" 149 | }, 150 | "dotnet": { 151 | "namespace": "Cdklabs.CdkEnterpriseIac", 152 | "packageId": "Cdklabs.CdkEnterpriseIac" 153 | } 154 | }, 155 | "tsc": { 156 | "outDir": "lib", 157 | "rootDir": "src" 158 | } 159 | }, 160 | "packageManager": "yarn@1.22.19+sha1.4ba7fc5c6e704fce2066ecbfb0b0d8976fe62447", 161 | "prettier": { 162 | "singleQuote": true, 163 | "semi": true, 164 | "trailingComma": "es5" 165 | }, 166 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 167 | } 168 | -------------------------------------------------------------------------------- /resources/constructs/ecsIsoServiceAutoscaler/ecs_scaling_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | import logging 4 | import os 5 | from datetime import datetime 6 | from typing import Any, Dict, List, Union, cast 7 | 8 | import boto3 9 | 10 | # Environment 11 | ECS_CLUSTER_NAME = os.environ.get("ECS_CLUSTER_NAME", "") 12 | ECS_SERVICE_NAME = os.environ.get("ECS_SERVICE_NAME", "") 13 | SCALE_ALARM_NAME = os.environ.get("SCALE_ALARM_NAME", "") 14 | SCALE_OUT_INCREMENT = int(os.environ.get("SCALE_OUT_INCREMENT", 1)) 15 | SCALE_IN_INCREMENT = int(os.environ.get("SCALE_IN_INCREMENT", 1)) 16 | SCALE_OUT_COOLDOWN = int(os.environ.get("SCALE_OUT_COOLDOWN", 60)) 17 | SCALE_IN_COOLDOWN = int(os.environ.get("SCALE_IN_COOLDOWN", 60)) 18 | MINIMUM_TASK_COUNT = int(os.environ.get("MINIMUM_TASK_COUNT", 1)) 19 | MAXIMUM_TASK_COUNT = int(os.environ.get("MAXIMUM_TASK_COUNT", 10)) 20 | 21 | # Logging 22 | logger = logging.getLogger() 23 | logger.setLevel(logging.INFO) 24 | 25 | # Clients 26 | cw_client = boto3.client("cloudwatch") 27 | ecs_client = boto3.client("ecs") 28 | 29 | 30 | def _get_alarm_states(alarm_name: str) -> List[Union[str, None]]: 31 | """ 32 | Gets a list of alarm states for a given alarm 33 | 34 | :param alarm_name: the alarm name to find states for 35 | :returns: list of alarm states, ex: ["OK", "ALARM"] 36 | """ 37 | 38 | composite_alarms = cw_client.describe_alarms( 39 | AlarmNames=[alarm_name], 40 | AlarmTypes=['CompositeAlarm'] 41 | ) 42 | metric_alarms = cw_client.describe_alarms( 43 | AlarmNames=[alarm_name], 44 | AlarmTypes=['MetricAlarm'] 45 | ) 46 | 47 | alarm_states = [] 48 | metric_alarm_list = metric_alarms.get("MetricAlarms", []) 49 | composite_alarm_list = composite_alarms.get("CompositeAlarms", []) 50 | # Including multiple handling here incase we with to extend to 51 | # manage alarm combinatorics here 52 | if metric_alarm_list: 53 | alarm_states = [x.get("StateValue") for x in metric_alarm_list] 54 | if composite_alarm_list: 55 | alarm_states = [x.get("StateValue") for x in composite_alarm_list] 56 | 57 | return alarm_states 58 | 59 | 60 | def _get_ecs_service(cluster_name: str, service_name: str) -> Dict[str, Any]: 61 | """ 62 | Gets the ECS Service boto3 response object 63 | 64 | :param cluster_name: name of ECS Cluster 65 | :param service_name: name of ECS Service 66 | :returns: boto3 dictionary response object for service 67 | """ 68 | services = ecs_client.describe_services( 69 | cluster=cluster_name, services=[service_name] 70 | ) 71 | service: Dict[str, Any] = cast(list, services["services"])[0] 72 | return service 73 | 74 | 75 | def _get_time_since_last_ecs_update( 76 | service: Dict[str, Any] 77 | ) -> float: 78 | """ 79 | Retrieve the time since the ECS Service was last updated 80 | 81 | :param service: the boto3 service response object 82 | :returns: time in seconds since ECS Service was last updated 83 | """ 84 | last_update = 0.0 85 | deployments = cast(list, service.get("deployments", None)) 86 | if len(deployments) == 1: 87 | current_deployment: Dict[str, Any] = deployments[0] 88 | last_updated = cast(datetime, current_deployment.get("updatedAt")) 89 | last_update = datetime.now().timestamp() - last_updated.timestamp() 90 | return last_update 91 | 92 | 93 | def _trigger_scaling_action( 94 | type_: str, 95 | increment: int, 96 | current_count: int, 97 | end_count: int, 98 | cooldown: int, 99 | last_update: float 100 | ) -> None: 101 | """ 102 | Triggers a scaling action for ECS 103 | 104 | :param type_: the type of scale to perform, should be "in" or "out" 105 | :param increment: the scaling increase or decrease increment to take 106 | :param end: the minimum or maximum task count to stop at 107 | :param cooldown: time elapsed from previous scaling action where no scaling 108 | action should be performed 109 | :param last_update: time the last update was performed on the service 110 | """ 111 | new_desired_count = current_count 112 | proposed_count = current_count 113 | 114 | if last_update >= cooldown: 115 | if current_count is not None: 116 | if type_.lower() == "out": 117 | proposed_count = current_count + increment 118 | new_desired_count = proposed_count if proposed_count <= end_count else end_count 119 | elif type_.lower() == "in": 120 | proposed_count = current_count - increment 121 | new_desired_count = proposed_count if proposed_count >= end_count else end_count 122 | 123 | if new_desired_count != current_count: 124 | ecs_client.update_service( 125 | cluster=ECS_CLUSTER_NAME, 126 | service=ECS_SERVICE_NAME, 127 | desiredCount=new_desired_count 128 | ) 129 | logger.info(f"Changed desired count from {current_count} to {new_desired_count}") 130 | else: 131 | logger.info( 132 | "Service is already at expected capacity, no action taken" 133 | ) 134 | else: 135 | logger.info( 136 | "Cooldown period has not been exceeded, no action taken" 137 | ) 138 | 139 | 140 | def handler(event, context): 141 | alarm_states = _get_alarm_states(SCALE_ALARM_NAME) 142 | logger.info(alarm_states) 143 | service = _get_ecs_service(ECS_CLUSTER_NAME, ECS_SERVICE_NAME) 144 | 145 | desired_count = service.get("desiredCount") 146 | running_count = service.get("runningCount") 147 | 148 | if desired_count != running_count: 149 | logger.info( 150 | "Task count has not caught up with desired count, no action taken" 151 | ) 152 | return 153 | 154 | last_updated = _get_time_since_last_ecs_update(service) 155 | 156 | if alarm_states is not None and "ALARM" in alarm_states: 157 | _trigger_scaling_action( 158 | type_="OUT", 159 | increment=SCALE_OUT_INCREMENT, 160 | current_count=desired_count, 161 | end_count=MAXIMUM_TASK_COUNT, 162 | cooldown=SCALE_OUT_COOLDOWN, 163 | last_update=last_updated 164 | ) 165 | elif alarm_states is not None and all(x == "OK" for x in alarm_states): 166 | _trigger_scaling_action( 167 | type_="IN", 168 | increment=SCALE_IN_INCREMENT, 169 | current_count=desired_count, 170 | end_count=MINIMUM_TASK_COUNT, 171 | cooldown=SCALE_IN_COOLDOWN, 172 | last_update=last_updated 173 | ) 174 | 175 | if __name__ == "__main__": 176 | handler({}, {}) -------------------------------------------------------------------------------- /rosetta/default.ts-fixture: -------------------------------------------------------------------------------- 1 | // Fixture with packages imported, but nothing else 2 | import { Construct } from 'constructs'; 3 | import { 4 | Stack, 5 | } from 'aws-cdk-lib'; 6 | 7 | class Fixture extends Stack { 8 | constructor(scope: Construct, id: string) { 9 | super(scope, id); 10 | 11 | /// here 12 | } 13 | } -------------------------------------------------------------------------------- /scripts/append-api-docs-to-readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | 6 | echo "=============================================================================================" 7 | echo "Copying API.md to the end of README.md" 8 | echo "=============================================================================================" 9 | echo "First removing existing API.md from README.md" 10 | sed -i.bak '/Generated API.md below:/,$d' README.md 11 | echo "=============================================================================================" 12 | echo "Adding back in divider text" 13 | echo 'Generated API.md below:' >> README.md 14 | echo '
' >> README.md 15 | echo ' Expand to view API docs' >> README.md 16 | echo '' >> README.md 17 | echo "=============================================================================================" 18 | echo "Appending API.md to the end of README.md" 19 | cat API.md >> README.md 20 | 21 | echo '
' >> README.md -------------------------------------------------------------------------------- /src/constructs/ecsIsoServiceAutoscaler/ecsIsoServiceAutoscaler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | 6 | import * as path from 'path'; 7 | import { Duration } from 'aws-cdk-lib'; 8 | import { AlarmBase } from 'aws-cdk-lib/aws-cloudwatch'; 9 | import { Cluster, IService } from 'aws-cdk-lib/aws-ecs'; 10 | import { Rule, Schedule } from 'aws-cdk-lib/aws-events'; 11 | import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets'; 12 | import { Effect, IRole, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 13 | import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; 14 | import { Construct } from 'constructs'; 15 | 16 | export interface EcsIsoServiceAutoscalerProps { 17 | /** 18 | * Optional IAM role to attach to the created lambda to adjust the desired count on the ECS Service 19 | * 20 | * Ensure this role has appropriate privileges. Example IAM policy statements: 21 | * ```json 22 | * { 23 | * "PolicyDocument": { 24 | * "Statement": [ 25 | * { 26 | * "Action": "cloudwatch:DescribeAlarms", 27 | * "Effect": "Allow", 28 | * "Resource": "*" 29 | * }, 30 | * { 31 | * "Action": [ 32 | * "ecs:DescribeServices", 33 | * "ecs:UpdateService" 34 | * ], 35 | * "Condition": { 36 | * "StringEquals": { 37 | * "ecs:cluster": "arn:${Partition}:ecs:${Region}:${Account}:cluster/${ClusterName}" 38 | * } 39 | * }, 40 | * "Effect": "Allow", 41 | * "Resource": "arn:${Partition}:ecs:${Region}:${Account}:service/${ClusterName}/${ServiceName}" 42 | * } 43 | * ], 44 | * "Version": "2012-10-17" 45 | * } 46 | *} 47 | * ``` 48 | * @default A role is created for you with least privilege IAM policy 49 | */ 50 | readonly role?: IRole; 51 | /** 52 | * The cluster the service you wish to scale resides in. 53 | */ 54 | readonly ecsCluster: Cluster; 55 | /** 56 | * The ECS service you wish to scale. 57 | */ 58 | readonly ecsService: IService; 59 | /** 60 | * The minimum number of tasks the service will have. 61 | * 62 | * @default 1 63 | */ 64 | readonly minimumTaskCount?: number; 65 | /** 66 | * The maximum number of tasks that the service will scale out to. 67 | * Note: This does not provide any protection from scaling out above the maximum allowed in your account, set this variable and manage account quotas appropriately. 68 | * 69 | * @default 10 70 | */ 71 | readonly maximumTaskCount?: number; 72 | /** 73 | * The Cloudwatch Alarm that will cause scaling actions to be invoked, whether it's in or not in alarm will determine scale up and down actions. 74 | * 75 | * Note: composite alarms can not be generated with CFN in all regions, while this allows you to pass in a composite alarm alarm creation is outside the scope of this construct 76 | */ 77 | readonly scaleAlarm: AlarmBase; 78 | /** 79 | * The number of tasks that will scale out on scale out alarm status 80 | * 81 | * @default 1 82 | */ 83 | readonly scaleOutIncrement?: number; 84 | /** 85 | * The number of tasks that will scale in on scale in alarm status 86 | * 87 | * @default 1 88 | */ 89 | readonly scaleInIncrement?: number; 90 | /** 91 | * How long will a the application wait before performing another scale out action. 92 | * 93 | * @default 60 seconds 94 | */ 95 | readonly scaleOutCooldown?: Duration; 96 | /** 97 | * How long will the application wait before performing another scale in action 98 | * 99 | * @default 60 seconds 100 | */ 101 | readonly scaleInCooldown?: Duration; 102 | } 103 | 104 | /** 105 | * Creates a EcsIsoServiceAutoscaler construct. This construct allows you to scale an ECS service in an ISO 106 | * region where classic ECS Autoscaling may not be available. 107 | * 108 | * @param scope The parent creating construct (usually `this`). 109 | * @param id The construct's name. 110 | * @param this A `EcsIsoServiceAutoscalerthis` interface. 111 | */ 112 | 113 | export class EcsIsoServiceAutoscaler extends Construct { 114 | public readonly ecsScalingManagerFunction: Function; 115 | 116 | constructor( 117 | scope: Construct, 118 | id: string, 119 | props: EcsIsoServiceAutoscalerProps 120 | ) { 121 | super(scope, id); 122 | 123 | const { 124 | minimumTaskCount = 1, 125 | maximumTaskCount = 10, 126 | scaleOutIncrement = 1, 127 | scaleInIncrement = 1, 128 | scaleOutCooldown = Duration.seconds(60), 129 | scaleInCooldown = Duration.seconds(60), 130 | } = props; 131 | 132 | this.ecsScalingManagerFunction = new Function( 133 | this, 134 | `${id}-EcsServiceScalingManager`, 135 | { 136 | code: Code.fromAsset( 137 | path.join( 138 | __dirname, 139 | '../../../resources/constructs/ecsIsoServiceAutoscaler' 140 | ) 141 | ), 142 | handler: 'ecs_scaling_manager.handler', 143 | runtime: Runtime.PYTHON_3_11, 144 | role: props.role || undefined, 145 | environment: { 146 | ECS_CLUSTER_NAME: props.ecsCluster.clusterName, 147 | ECS_SERVICE_NAME: props.ecsService.serviceName, 148 | MINIMUM_TASK_COUNT: minimumTaskCount.toString(), 149 | MAXIMUM_TASK_COUNT: maximumTaskCount.toString(), 150 | SCALE_ALARM_NAME: props.scaleAlarm.alarmName, 151 | SCALE_OUT_INCREMENT: scaleOutIncrement.toString(), 152 | SCALE_OUT_COOLDOWN: scaleOutCooldown.toSeconds().toString(), 153 | SCALE_IN_INCREMENT: scaleInIncrement.toString(), 154 | SCALE_IN_COOLDOWN: scaleInCooldown.toSeconds().toString(), 155 | }, 156 | } 157 | ); 158 | 159 | new Rule(this, `${id}-EcsScalingManagerSchedule`, { 160 | description: `Kicks off Lambda to adjust ECS scaling for service: ${props.ecsService.serviceName}`, 161 | enabled: true, 162 | schedule: Schedule.rate(Duration.minutes(1)), 163 | targets: [new LambdaFunction(this.ecsScalingManagerFunction)], 164 | }); 165 | 166 | if (!props.role) { 167 | // Set permissions for ecsScalingManagerFunction role 168 | this.ecsScalingManagerFunction.addToRolePolicy( 169 | new PolicyStatement({ 170 | actions: ['cloudwatch:DescribeAlarms'], 171 | effect: Effect.ALLOW, 172 | // Required to be * and can not scope down due to composite alarm role constraints listed here: 173 | // https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/permissions-reference-cw.html 174 | // "cloudwatch:DescribeAlarms permission must have a * scope. You can't return information about 175 | // composite alarms if your cloudwatch:DescribeAlarms permission has a narrower scope." 176 | resources: ['*'], 177 | }) 178 | ); 179 | 180 | this.ecsScalingManagerFunction.addToRolePolicy( 181 | new PolicyStatement({ 182 | actions: ['ecs:DescribeServices', 'ecs:UpdateService'], 183 | effect: Effect.ALLOW, 184 | resources: [props.ecsService.serviceArn], 185 | conditions: { 186 | StringLike: { 187 | 'ecs:cluster': props.ecsCluster.clusterArn, 188 | }, 189 | }, 190 | }) 191 | ); 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/constructs/enterpriseDnsResolver/enterpriseDnsResolver.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { IVpc, Peer, Port, SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2'; 6 | import { 7 | CfnResolverEndpoint, 8 | CfnResolverRule, 9 | CfnResolverRuleAssociation, 10 | } from 'aws-cdk-lib/aws-route53resolver'; 11 | import { Construct } from 'constructs'; 12 | 13 | export interface EnterpriseDnsResolverProps { 14 | /** 15 | * Vpc or IVpc to associate resolver rules with 16 | */ 17 | readonly vpc: Vpc | IVpc; 18 | /** 19 | * List of IPs for enterprise DNS servers 20 | */ 21 | readonly enterpriseDnsIpAddresses: string[]; 22 | } 23 | 24 | export class EnterpriseDnsResolver extends Construct { 25 | private readonly _vpc: Vpc | IVpc; 26 | private readonly _enterpriseDnsIpAddresses: string[]; 27 | 28 | constructor(scope: Construct, id: string, props: EnterpriseDnsResolverProps) { 29 | super(scope, id); 30 | 31 | this._vpc = props.vpc; 32 | this._enterpriseDnsIpAddresses = props.enterpriseDnsIpAddresses; 33 | 34 | const sg = new SecurityGroup(this, 'resolverEndpointSecurityGroup', { 35 | vpc: this._vpc, 36 | }); 37 | sg.addIngressRule( 38 | Peer.ipv4(this._vpc.vpcCidrBlock), 39 | Port.tcp(53), 40 | 'tcp port 53 for route53 resolver' 41 | ); 42 | sg.addIngressRule( 43 | Peer.ipv4(this._vpc.vpcCidrBlock), 44 | Port.udp(53), 45 | 'udp port 53 for route53 resolver' 46 | ); 47 | 48 | let ipAddresses: CfnResolverEndpoint.IpAddressRequestProperty[] = []; 49 | const combinedSubnets = [ 50 | ...this._vpc.publicSubnets, 51 | ...this._vpc.privateSubnets, 52 | ...this._vpc.isolatedSubnets, 53 | ]; 54 | 55 | for (let i = 0; i < combinedSubnets.length; i++) { 56 | const subnet = combinedSubnets[i]; 57 | ipAddresses.push({ 58 | subnetId: subnet.subnetId, 59 | }); 60 | } 61 | 62 | const resolverEndpoint = new CfnResolverEndpoint( 63 | this, 64 | 'EnterpriseDnsResolverEndpoint', 65 | { 66 | direction: 'OUTBOUND', 67 | securityGroupIds: [sg.securityGroupId], 68 | ipAddresses, 69 | } 70 | ); 71 | 72 | // system rule 73 | const systemRule = new CfnResolverRule(this, 'AmazonInternalRule', { 74 | domainName: 'amazon.internal', 75 | ruleType: 'SYSTEM', 76 | }); 77 | 78 | let targetIps: CfnResolverRule.TargetAddressProperty[] = []; 79 | for (let i = 0; i < this._enterpriseDnsIpAddresses.length; i++) { 80 | const ip = this._enterpriseDnsIpAddresses[i]; 81 | targetIps.push({ 82 | ip, 83 | port: '53', 84 | }); 85 | } 86 | // Enterprise DNS rule 87 | const enterpriseRule = new CfnResolverRule(this, 'EnterpriseDnsRule', { 88 | domainName: '.', 89 | resolverEndpointId: resolverEndpoint.attrResolverEndpointId, 90 | ruleType: 'FORWARD', 91 | targetIps, 92 | }); 93 | 94 | new CfnResolverRuleAssociation(this, 'SystemRuleAssociation', { 95 | resolverRuleId: systemRule.attrResolverRuleId, 96 | vpcId: this._vpc.vpcId, 97 | }); 98 | 99 | new CfnResolverRuleAssociation(this, 'EnterpriseDnsRuleAssociation', { 100 | resolverRuleId: enterpriseRule.attrResolverRuleId, 101 | vpcId: this._vpc.vpcId, 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/constructs/populateProvidedVpc/populateProvidedVpc.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Fn, Stack, Tags } from 'aws-cdk-lib'; 6 | import { 7 | CfnSubnet, 8 | CfnSubnetRouteTableAssociation, 9 | SubnetType, 10 | } from 'aws-cdk-lib/aws-ec2'; 11 | import { Construct } from 'constructs'; 12 | 13 | export interface SubnetConfig { 14 | /** 15 | * Logical group name of a subnet 16 | * @example app 17 | * @example db 18 | */ 19 | readonly groupName: string; 20 | /** 21 | * Which availability zone the subnet should be in 22 | * 23 | */ 24 | readonly availabilityZone: string; 25 | /** 26 | * Cidr range of the subnet to create 27 | */ 28 | readonly cidrRange: string; 29 | /** 30 | * What [SubnetType](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.SubnetType.html) to use 31 | * 32 | * This will govern the `aws-cdk:subnet-type` tag on the subnet 33 | * 34 | * SubnetType | `aws-cdk:subnet-type` tag value 35 | * --- | --- 36 | * `PRIVATE_ISOLATED` | 'Isolated' 37 | * `PRIVATE_WITH_EGRESS` | 'Private' 38 | * `PUBLIC` | 'Public' 39 | */ 40 | readonly subnetType: SubnetType; 41 | } 42 | 43 | export interface PopulateWithConfigProps { 44 | /** 45 | * ID of the VPC provided that needs to be populated 46 | */ 47 | readonly vpcId: string; 48 | /** 49 | * Route table ID for a provided route table with routes to enterprise network 50 | * 51 | * Both subnetType.PUBLIC and subnetType.PRIVATE_WITH_EGRESS will use this property 52 | */ 53 | readonly privateRouteTableId: string; 54 | /** 55 | * Local route table ID, with routes only to local VPC 56 | */ 57 | readonly localRouteTableId: string; 58 | /** 59 | * List of Subnet configs to provision to provision 60 | */ 61 | readonly subnetConfig: SubnetConfig[]; 62 | } 63 | 64 | /** 65 | * Populate a provided VPC with subnets based on a provided configuration 66 | * 67 | * @example 68 | * const mySubnetConfig: SubnetConfig[] = [ 69 | { 70 | groupName: 'app', 71 | cidrRange: '172.31.0.0/27', 72 | availabilityZone: 'a', 73 | subnetType: subnetType.PUBLIC, 74 | }, 75 | { 76 | groupName: 'app', 77 | cidrRange: '172.31.0.32/27', 78 | availabilityZone: 'b', 79 | subnetType: subnetType.PUBLIC, 80 | }, 81 | { 82 | groupName: 'db', 83 | cidrRange: '172.31.0.64/27', 84 | availabilityZone: 'a', 85 | subnetType: subnetType.PRIVATE_WITH_EGRESS, 86 | }, 87 | { 88 | groupName: 'db', 89 | cidrRange: '172.31.0.96/27', 90 | availabilityZone: 'b', 91 | subnetType: subnetType.PRIVATE_WITH_EGRESS, 92 | }, 93 | { 94 | groupName: 'iso', 95 | cidrRange: '172.31.0.128/26', 96 | availabilityZone: 'a', 97 | subnetType: subnetType.PRIVATE_ISOLATED, 98 | }, 99 | { 100 | groupName: 'iso', 101 | cidrRange: '172.31.0.196/26', 102 | availabilityZone: 'b', 103 | subnetType: subnetType.PRIVATE_ISOLATED, 104 | }, 105 | ]; 106 | * new PopulateWithConfig(this, "vpcPopulater", { 107 | * vpcId: 'vpc-abcdefg1234567', 108 | * privateRouteTableId: 'rt-abcdefg123456', 109 | * localRouteTableId: 'rt-123456abcdefg', 110 | * subnetConfig: mySubnetConfig, 111 | * }) 112 | */ 113 | export class PopulateWithConfig extends Construct { 114 | private readonly _vpcId: string; 115 | private readonly _privateRouteTableId: string; 116 | private readonly _localRouteTableId: string; 117 | private readonly _subnetConfig: SubnetConfig[]; 118 | 119 | constructor(scope: Construct, id: string, props: PopulateWithConfigProps) { 120 | super(scope, id); 121 | 122 | this._vpcId = props.vpcId; 123 | this._privateRouteTableId = props.privateRouteTableId; 124 | this._localRouteTableId = props.localRouteTableId; 125 | this._subnetConfig = props.subnetConfig; 126 | 127 | for (const key in this._subnetConfig) { 128 | if (Object.prototype.hasOwnProperty.call(this._subnetConfig, key)) { 129 | const subnet = this._subnetConfig[key]; 130 | 131 | const sub = new CfnSubnet( 132 | this, 133 | `Subnet${subnet.groupName}-${subnet.availabilityZone}`, 134 | { 135 | availabilityZone: `${Stack.of(this).region}${ 136 | subnet.availabilityZone 137 | }`, 138 | vpcId: this._vpcId, 139 | cidrBlock: subnet.cidrRange, 140 | } 141 | ); 142 | Tags.of(sub).add('aws-cdk:subnet-type', subnet.subnetType); 143 | Tags.of(sub).add( 144 | 'Name', 145 | `subnet-${subnet.groupName}-${subnet.availabilityZone}` 146 | ); 147 | 148 | new CfnSubnetRouteTableAssociation( 149 | this, 150 | `SubnetReAssoc${subnet.groupName}-${subnet.availabilityZone}`, 151 | { 152 | routeTableId: 153 | subnet.subnetType == 'Isolated' 154 | ? this._localRouteTableId 155 | : this._privateRouteTableId, 156 | subnetId: sub.ref, 157 | } 158 | ); 159 | } 160 | } 161 | } 162 | } 163 | 164 | export interface SplitVpcEvenlyProps { 165 | /** 166 | * ID of the existing VPC you're trying to populate 167 | */ 168 | readonly vpcId: string; 169 | /** 170 | * CIDR range of the VPC you're populating 171 | */ 172 | readonly vpcCidr: string; 173 | /** 174 | * Route Table ID that will be attached to each subnet created 175 | */ 176 | readonly routeTableId: string; 177 | /** 178 | * `cidrBits` argument for the [`Fn::Cidr`](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-cidr.html) Cloudformation intrinsic function 179 | * 180 | * @default '6' 181 | */ 182 | readonly cidrBits?: string; 183 | /** 184 | * Number of AZs to evenly split into 185 | * 186 | * @default 3 187 | */ 188 | readonly numberOfAzs?: number; 189 | /** 190 | * @default subnetType.PRIVATE 191 | */ 192 | readonly subnetType?: SubnetType; 193 | } 194 | /** 195 | * Splits a VPC evenly between a provided number of AZs (3 if not defined), and attaches a provided route table to each, and labels 196 | * 197 | * @example 198 | * new SplitVpcEvenly(this, 'evenSplitVpc', { 199 | * vpcId: 'vpc-abcdefg123456', 200 | * vpcCidr: '172.16.0.0/24', 201 | * routeTableId: 'rt-abcdefgh123456', 202 | * }); 203 | * @example 204 | * // with more specific properties 205 | * new SplitVpcEvenly(this, 'evenSplitVpc', { 206 | * vpcId: 'vpc-abcdefg123456', 207 | * vpcCidr: '172.16.0.0/16', 208 | * routeTableId: 'rt-abcdefgh123456', 209 | * cidrBits: '10', 210 | * numberOfAzs: 4, 211 | * subnetType: subnetType.PRIVATE_ISOLATED, 212 | * }); 213 | */ 214 | export class SplitVpcEvenly extends Construct { 215 | private readonly _vpcId: string; 216 | private readonly _vpcCidr: string; 217 | private readonly _routeTableId: string; 218 | private readonly _cidrBits?: string; 219 | private readonly _numberOfAzs?: number; 220 | private readonly _subnetType?: SubnetType; 221 | 222 | constructor(scope: Construct, id: string, props: SplitVpcEvenlyProps) { 223 | super(scope, id); 224 | 225 | this._vpcId = props.vpcId; 226 | this._vpcCidr = props.vpcCidr; 227 | this._routeTableId = props.routeTableId; 228 | this._cidrBits = props.cidrBits || '6'; 229 | this._numberOfAzs = props.numberOfAzs || 3; 230 | this._subnetType = props.subnetType || SubnetType.PRIVATE_WITH_EGRESS; 231 | 232 | // Throw error if > 6 AZs 233 | if (this._numberOfAzs < 2 || this._numberOfAzs > 6) { 234 | throw new Error('numberOfAzs must be between 2 and 6'); 235 | } 236 | 237 | // based on number of az, create an array of az strings 238 | const azs: string[] = []; 239 | for (let i = 0; i < this._numberOfAzs; i++) { 240 | azs.push(String.fromCharCode(97 + i)); 241 | } 242 | 243 | azs.forEach((val, i) => { 244 | const sub = new CfnSubnet(this, `TgwSubnet${val}`, { 245 | availabilityZone: Fn.select(i, Fn.getAzs()), 246 | vpcId: this._vpcId, 247 | cidrBlock: Fn.select( 248 | i, 249 | Fn.cidr(this._vpcCidr, this._numberOfAzs!, this._cidrBits) 250 | ), 251 | }); 252 | Tags.of(sub).add('aws-cdk:subnet-type', this._subnetType!); 253 | Tags.of(sub).add('Name', `subnet-${val}`); 254 | 255 | new CfnSubnetRouteTableAssociation(this, `SubnetRtAssoc${val}`, { 256 | routeTableId: this._routeTableId, 257 | subnetId: sub.ref, 258 | }); 259 | }); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | export * from './patches/addPermissionsBoundary'; 6 | export * from './patches/removeTags'; 7 | export * from './patches/addLambdaEnvironmentVariables'; 8 | export * from './patches/removePublicAccessBlockConfiguration'; 9 | export * from './patches/setApiGatewayEndpointConfiguration'; 10 | export * from './patches/addCfnInitProxy'; 11 | export * from './patches/convertInlinePoliciesToManaged'; 12 | export * from './constructs/populateProvidedVpc/populateProvidedVpc'; 13 | export * from './constructs/ecsIsoServiceAutoscaler/ecsIsoServiceAutoscaler'; 14 | export * from './constructs/enterpriseDnsResolver/enterpriseDnsResolver'; 15 | export * from './patches/resource-extractor/resourceExtractor'; 16 | export { ResourceTransform } from './patches/resource-extractor/resourceTransformer'; 17 | -------------------------------------------------------------------------------- /src/patches/addCfnInitProxy.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { CfnResource, IAspect, Stack } from 'aws-cdk-lib'; 6 | import { CfnLaunchConfiguration } from 'aws-cdk-lib/aws-autoscaling'; 7 | import { CfnInstance } from 'aws-cdk-lib/aws-ec2'; 8 | import { ISecret } from 'aws-cdk-lib/aws-secretsmanager'; 9 | 10 | import { IConstruct } from 'constructs'; 11 | 12 | /** 13 | * Whether an http-proxy or https-proxy 14 | */ 15 | export enum ProxyType { 16 | /** 17 | * --http-proxy 18 | */ 19 | 'HTTP', 20 | /** 21 | * --https-proxy 22 | */ 23 | 'HTTPS', 24 | } 25 | 26 | /** 27 | * Properties for the proxy server to use with cfn helper commands 28 | */ 29 | export interface AddCfnInitProxyProps { 30 | /** 31 | * host of your proxy 32 | * @example example.com 33 | */ 34 | readonly proxyHost: string; 35 | /** 36 | * proxy port 37 | * @example 8080 38 | */ 39 | readonly proxyPort: number; 40 | /** 41 | * Proxy Type 42 | * @example ProxyType.HTTPS 43 | * @default ProxyType.HTTP 44 | */ 45 | readonly proxyType?: ProxyType; 46 | /** 47 | * JSON secret containing `user` and `password` properties to use if your proxy requires credentials 48 | * `http://user:password@host:port` could contain sensitive data, so using a Secret. 49 | * 50 | * Note that while the `user` and `password` won't be visible in the cloudformation template 51 | * they **will** be in plain text inside your `UserData` 52 | * 53 | * @example 54 | * const secret = new Secret(stack, 'TestSecret', { 55 | secretObjectValue: { 56 | user: SecretValue, 57 | password: SecretValue, 58 | }, 59 | }); 60 | */ 61 | readonly proxyCredentials?: ISecret; 62 | } 63 | /** 64 | * Add proxy configuration to Cloudformation helper functions 65 | * 66 | * @extends IAspect 67 | */ 68 | export class AddCfnInitProxy implements IAspect { 69 | private readonly _proxyHost: string; 70 | private readonly _proxyPort: number; 71 | private readonly _proxyType?: ProxyType; 72 | private readonly _proxyCredentials?: ISecret; 73 | private readonly _initResourceTypes: string[]; 74 | private readonly _proxyValue: string[]; 75 | 76 | constructor(props: AddCfnInitProxyProps) { 77 | this._proxyHost = props.proxyHost; 78 | this._proxyPort = props.proxyPort; 79 | this._proxyType = props.proxyType || ProxyType.HTTP; 80 | this._proxyCredentials = props.proxyCredentials || undefined; 81 | this._proxyValue = this.determineProxyValue(); 82 | this._initResourceTypes = [ 83 | 'AWS::AutoScaling::LaunchConfiguration', 84 | 'AWS::EC2::Instance', 85 | ]; 86 | } 87 | 88 | public visit(node: IConstruct): void { 89 | if ( 90 | node instanceof CfnResource && 91 | this._initResourceTypes.includes(node.cfnResourceType) 92 | ) { 93 | let userData; 94 | if (node.cfnResourceType == CfnInstance.CFN_RESOURCE_TYPE_NAME) { 95 | const instanceNode = node as CfnInstance; 96 | userData = Stack.of(node).resolve(instanceNode.userData); 97 | } else if ( 98 | node.cfnResourceType == CfnLaunchConfiguration.CFN_RESOURCE_TYPE_NAME 99 | ) { 100 | const launchConfigNode = node as CfnLaunchConfiguration; 101 | userData = Stack.of(node).resolve(launchConfigNode.userData); 102 | } 103 | 104 | const commandList: string[] = userData['Fn::Base64']['Fn::Join'][1]; 105 | 106 | const resourceIndexes = this.indexOfList('--resource', commandList); 107 | 108 | for ( 109 | let i = 0, j = 0; 110 | i < resourceIndexes.length; 111 | i++, j += this._proxyValue.length 112 | ) { 113 | const lineIdx = resourceIndexes[i] + j; 114 | commandList.splice(lineIdx, 0, ...this._proxyValue); 115 | } 116 | node.addPropertyOverride('UserData.Fn::Base64.Fn::Join', [ 117 | '', 118 | commandList, 119 | ]); 120 | } 121 | } 122 | 123 | private determineProxyValue(): string[] { 124 | const result = []; 125 | if (this._proxyType == ProxyType.HTTP) { 126 | result.push(' --http-proxy http://'); 127 | } else { 128 | result.push(' --https-proxy https://'); 129 | } 130 | 131 | if (this._proxyCredentials) { 132 | result.push( 133 | `${this._proxyCredentials 134 | .secretValueFromJson('user') 135 | .unsafeUnwrap()}:${this._proxyCredentials 136 | .secretValueFromJson('password') 137 | .unsafeUnwrap()}@` 138 | ); 139 | } 140 | result.push(`${this._proxyHost}:${this._proxyPort}`); 141 | 142 | return result; 143 | } 144 | 145 | private indexOfList(needle: string, haystack: string[]): number[] { 146 | const result = []; 147 | for (let idx = 0; idx < haystack.length; idx++) { 148 | const command: any = haystack[idx]; 149 | if (command instanceof Object) continue; 150 | if (command.indexOf(needle) >= 0) { 151 | result.push(idx); 152 | } 153 | } 154 | return result; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/patches/addLambdaEnvironmentVariables.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { IAspect } from 'aws-cdk-lib'; 6 | import { Function } from 'aws-cdk-lib/aws-lambda'; 7 | import { IConstruct } from 'constructs'; 8 | 9 | /** 10 | * Add one or more environment variables to _all_ lambda functions within a scope 11 | * 12 | * @extends IAspect 13 | */ 14 | export class AddLambdaEnvironmentVariables implements IAspect { 15 | /** 16 | * Key value pairs of environment variables to add to all lambda functions within the scope 17 | */ 18 | private _environmentKeyValues: { [key: string]: string }; 19 | 20 | /** 21 | * 22 | * @param props {[key: string]: string} props - Key Value pair(s) for environment variables to add to all lambda functions 23 | */ 24 | constructor(props: { [key: string]: string }) { 25 | this._environmentKeyValues = props; 26 | } 27 | 28 | public visit(node: IConstruct): void { 29 | if (node instanceof Function) { 30 | const lmb: Function = node; 31 | for (const [key, value] of Object.entries(this._environmentKeyValues)) { 32 | lmb.addEnvironment(key, value); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/patches/addPermissionsBoundary.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { createHash } from 'crypto'; 6 | import { CfnResource, IAspect, Stack, Token } from 'aws-cdk-lib'; 7 | import { 8 | CfnInstanceProfile, 9 | CfnManagedPolicy, 10 | CfnPolicy, 11 | } from 'aws-cdk-lib/aws-iam'; 12 | import { IConstruct } from 'constructs'; 13 | import { getResourceId } from '../utils/utils'; 14 | 15 | /** 16 | * Properties to pass to the AddPermissionBoundary 17 | * 18 | * @interface AddPermissionBoundaryProps 19 | * 20 | */ 21 | export interface AddPermissionBoundaryProps { 22 | /** 23 | * Name of Permissions Boundary Policy to add to all IAM roles 24 | */ 25 | readonly permissionsBoundaryPolicyName: string; 26 | /** 27 | * A prefix to prepend to the name of IAM Roles (Default: ''). 28 | */ 29 | readonly rolePrefix?: string; 30 | /** 31 | * A prefix to prepend to the name of the IAM Policies and ManagedPolicies (Default: ''). 32 | */ 33 | readonly policyPrefix?: string; 34 | /** 35 | * A prefix to prepend to the name of the IAM InstanceProfiles (Default: ''). 36 | */ 37 | readonly instanceProfilePrefix?: string; 38 | /** 39 | * An IAM path to add to all IAM roles (Default: ''). 40 | */ 41 | readonly rolePath?: string; 42 | } 43 | 44 | /** 45 | * A patch for Adding Permissions Boundaries to all IAM roles 46 | * 47 | * Additional options for adding prefixes to IAM role, policy and instance profile names 48 | * 49 | * Can account for non commercial partitions (e.g. aws-gov, aws-cn) 50 | */ 51 | export class AddPermissionBoundary implements IAspect { 52 | private _permissionsBoundaryPolicyName: string; 53 | private _rolePrefix: string; 54 | private _policyPrefix: string; 55 | private _instanceProfilePrefix: string; 56 | private _rolePath: string; 57 | 58 | constructor(props: AddPermissionBoundaryProps) { 59 | this._permissionsBoundaryPolicyName = props.permissionsBoundaryPolicyName; 60 | this._rolePrefix = props.rolePrefix || ''; 61 | this._policyPrefix = props.policyPrefix || ''; 62 | this._instanceProfilePrefix = props.instanceProfilePrefix || ''; 63 | this._rolePath = props.rolePath || ''; 64 | } 65 | 66 | public checkAndOverride( 67 | node: CfnResource, 68 | prefix: string, 69 | length: number, 70 | cfnProp: string, 71 | cdkProp?: string 72 | ): void { 73 | if (!cdkProp?.startsWith(prefix)) { 74 | let policySuffix; 75 | if (cdkProp != undefined) { 76 | policySuffix = !Token.isUnresolved(cdkProp) 77 | ? (cdkProp as string) 78 | : getResourceId(node.node.path); 79 | } else if (cdkProp == undefined && prefix != '') { 80 | policySuffix = getResourceId(node.node.path); 81 | } else { 82 | return; 83 | } 84 | const uniqness_length = 8; 85 | let suffix = `${policySuffix.replace(/\s/g, '')}`.substring( 86 | 0, 87 | length - uniqness_length - prefix.length 88 | ); 89 | const region = Stack.of(node).region; 90 | const hash = createHash('shake256'); 91 | hash.update(`${node.node.path}-${region}`); 92 | let hash_value = hash.copy().digest('hex'); 93 | const charUniqness8 = hash_value.substring( 94 | hash_value.length - 8, 95 | hash_value.length 96 | ); 97 | let newSuffix = prefix + suffix + charUniqness8; 98 | node.addPropertyOverride(cfnProp, `${newSuffix}`.substring(0, length)); 99 | } 100 | } 101 | 102 | public visit(node: IConstruct): void { 103 | if (node instanceof CfnResource) { 104 | const cfnResourceNode: CfnResource = node; 105 | if (cfnResourceNode.cfnResourceType == 'AWS::IAM::Role') { 106 | const permissionsBoundaryPolicyArn = Stack.of(node).formatArn({ 107 | service: 'iam', 108 | resource: 'policy', 109 | region: '', 110 | resourceName: this._permissionsBoundaryPolicyName, 111 | }); 112 | node.addPropertyOverride( 113 | 'PermissionsBoundary', 114 | permissionsBoundaryPolicyArn 115 | ); 116 | if (this._rolePath) { 117 | node.addPropertyOverride('Path', this._rolePath); 118 | } 119 | const roleName = 120 | // eslint-disable-next-line dot-notation 121 | cfnResourceNode['cfnProperties'].roleName || 122 | cfnResourceNode.logicalId; 123 | this.checkAndOverride(node, this._rolePrefix, 64, 'RoleName', roleName); 124 | } 125 | } 126 | if (node instanceof CfnPolicy) { 127 | this.checkAndOverride( 128 | node, 129 | this._policyPrefix, 130 | 128, 131 | 'PolicyName', 132 | node.policyName 133 | ); 134 | } else if (node instanceof CfnManagedPolicy) { 135 | this.checkAndOverride( 136 | node, 137 | this._policyPrefix, 138 | 128, 139 | 'ManagedPolicyName', 140 | node.managedPolicyName 141 | ); 142 | } else if (node instanceof CfnInstanceProfile) { 143 | this.checkAndOverride( 144 | node, 145 | this._instanceProfilePrefix, 146 | 128, 147 | 'InstanceProfileName', 148 | node.instanceProfileName 149 | ); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/patches/convertInlinePoliciesToManaged.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Stack, IAspect, aws_iam as iam } from 'aws-cdk-lib'; 6 | import { Construct, IConstruct } from 'constructs'; 7 | 8 | /** 9 | * Patch for turning all Policies into ConvertInlinePoliciesToManaged 10 | * 11 | * Some users have policies in place that make it impossible to create inline policies. Instead, 12 | * they must use managed policies. 13 | * 14 | * Note that order matters with this aspect. Specifically, it should generally be added first. 15 | * This is because other aspects may add overrides that would be lost if applied before 16 | * this aspect since the original aspect is removed and replaced. 17 | * 18 | * @example 19 | * // Replace all AWS::IAM::Policy resources with equivalent AWS::IAM::ManagedPolicy 20 | * Aspects.of(stack).add(new ConvertInlinePoliciesToManaged()) 21 | */ 22 | export class ConvertInlinePoliciesToManaged implements IAspect { 23 | public visit(node: IConstruct): void { 24 | if (node instanceof iam.CfnPolicy) { 25 | const policy = node as iam.CfnPolicy; 26 | const logicalId = Stack.of(policy).resolve(policy.logicalId); 27 | const policyDocument = Stack.of(policy).resolve(policy.policyDocument); 28 | const parent = policy.node.scope as Construct; 29 | parent.node.tryRemoveChild(policy.node.id); 30 | 31 | const resource = new iam.CfnManagedPolicy(parent, logicalId, { 32 | managedPolicyName: Stack.of(policy).resolve(policy.policyName), 33 | groups: policy.groups, 34 | roles: policy.roles, 35 | policyDocument, 36 | }); 37 | resource.overrideLogicalId(logicalId); 38 | 39 | const overrides = (node as any).rawOverrides; 40 | if (overrides?.Properties?.PolicyName) { 41 | resource.addPropertyOverride( 42 | 'ManagedPolicyName', 43 | overrides?.Properties?.PolicyName 44 | ); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/patches/removePublicAccessBlockConfiguration.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { IAspect } from 'aws-cdk-lib'; 6 | import { Bucket, CfnBucket } from 'aws-cdk-lib/aws-s3'; 7 | import { IConstruct } from 'constructs'; 8 | 9 | /** 10 | * Looks for S3 Buckets, and removes the `PublicAccessBlockConfiguration` property. 11 | * 12 | * For use in regions where Cloudformation doesn't support this property 13 | */ 14 | export class RemovePublicAccessBlockConfiguration implements IAspect { 15 | public visit(node: IConstruct): void { 16 | if (node instanceof Bucket) { 17 | const cfnBucket = node.node.defaultChild as CfnBucket; 18 | cfnBucket.addPropertyDeletionOverride('PublicAccessBlockConfiguration'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/patches/removeTags.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { CfnResource, IAspect } from 'aws-cdk-lib'; 6 | import { IConstruct } from 'constructs'; 7 | 8 | export interface RemoveTagsProps { 9 | /** 10 | * Name of Cloudformation resource Type (e.g. 'AWS::Lambda::Function') 11 | */ 12 | readonly cloudformationResource: string; 13 | 14 | /** 15 | * Name of the tag property to remove from the resource 16 | * 17 | * @default Tags 18 | */ 19 | readonly tagPropertyName?: string; 20 | } 21 | 22 | /** 23 | * Patch for removing tags from a specific Cloudformation Resource 24 | * 25 | * In some regions, the 'Tags' property isn't supported in Cloudformation. This patch makes it easy to remove 26 | * 27 | * @example 28 | * // Remove tags on a resource 29 | * Aspects.of(stack).add(new RemoveTags({ 30 | * cloudformationResource: 'AWS::ECS::Cluster', 31 | * })); 32 | * // Remove tags without the standard 'Tags' name 33 | * Aspects.of(stack).add(new RemoveTags({ 34 | * cloudformationResource: 'AWS::Backup::BackupPlan', 35 | * tagPropertyName: 'BackupPlanTags', 36 | * })); 37 | */ 38 | export class RemoveTags implements IAspect { 39 | private _cloudformationResource: string; 40 | private _tagPropertyName: string; 41 | 42 | constructor(props: RemoveTagsProps) { 43 | this._cloudformationResource = props.cloudformationResource; 44 | this._tagPropertyName = props.tagPropertyName || 'Tags'; 45 | } 46 | 47 | public visit(node: IConstruct): void { 48 | if ( 49 | CfnResource.isCfnResource(node) && 50 | node.cfnResourceType == this._cloudformationResource 51 | ) { 52 | node.addPropertyDeletionOverride(this._tagPropertyName); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/patches/resource-extractor/cfnStore.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import * as cp from 'child_process'; 6 | import type { Stack } from '@aws-sdk/client-cloudformation'; 7 | import { CfnResource, Fn } from 'aws-cdk-lib'; 8 | import { CloudFormationStackArtifact } from 'aws-cdk-lib/cx-api'; 9 | import { Flattener } from './flattener'; 10 | import { ResourceExtractorShareMethod } from './resourceExtractor'; 11 | import { FlatJson, Json } from './types'; 12 | 13 | export interface CfnStoreProps { 14 | readonly stackArtifacts: CloudFormationStackArtifact[]; 15 | readonly valueShareMethod: ResourceExtractorShareMethod; 16 | readonly extractedStackName: string; 17 | readonly region: string; 18 | } 19 | 20 | export class CfnStore { 21 | public readonly templates: Json = {}; 22 | public readonly flatTemplates: FlatJson; 23 | public readonly extractedStackExports: FlatJson = {}; 24 | public readonly fnJoins: Json = {}; 25 | 26 | constructor(props: CfnStoreProps) { 27 | /** Save CloudFormation templates for future lookup */ 28 | for (const item of props.stackArtifacts) { 29 | this.templates[item.stackName] = item.template; 30 | } 31 | this.flatTemplates = Flattener.flattenObject(this.templates); 32 | 33 | /** Save Value Map if we are using the `API_LOOKUP` sharing method */ 34 | if (props.valueShareMethod == ResourceExtractorShareMethod.API_LOOKUP) { 35 | const stack = this.describeStack(props.extractedStackName, props.region); 36 | if (stack) { 37 | this.extractedStackExports = this.createExportMap(stack); 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Determine what the export value should be, for example if it should be a 44 | * `Ref` or `Fn::GetAtt` 45 | * 46 | * @param resource 47 | * @param flattendKey 48 | * @returns 49 | */ 50 | public determineExportValue(resource: CfnResource, flattendKey: string) { 51 | const splitKey = flattendKey.split('.'); 52 | 53 | if (splitKey.slice(-1)[0] == 'Ref') { 54 | return resource.ref; 55 | } else if (splitKey.slice(-2)[0] == 'Fn::GetAtt') { 56 | const attToGet = Flattener.getValueByPath( 57 | this.templates, 58 | splitKey.slice(0, -1).concat(['1']).join('.') 59 | ); 60 | return resource.getAtt(attToGet); 61 | } else if (splitKey.slice(-2)[0] == 'DependsOn') { 62 | return false; 63 | } else { 64 | throw new Error(`Can't determine export value for ${flattendKey}`); 65 | } 66 | } 67 | 68 | /** 69 | * Retrieve a Stack Name from a given Logical ID 70 | * 71 | * @param logicalId the Logical ID of the Resource to find 72 | * @returns the Stack Name that the Logical ID is found to be in 73 | */ 74 | public getStackNameFromLogicalId(logicalId: string): string { 75 | const stackTypeKey = Object.keys(this.flatTemplates).filter( 76 | (key) => key.indexOf(`${logicalId}.Type`) > -1 77 | )[0]; 78 | const stackName = stackTypeKey.split('.')[0]; 79 | return stackName; 80 | } 81 | 82 | /** 83 | * Retrieve a Resource Type from a given Logical ID 84 | * 85 | * @param logicalId the logical ID of the Resource to find 86 | * @returns the Resource Type of the provided Logical ID 87 | */ 88 | public getResourceTypeFromLogicalId(logicalId: string): string { 89 | const typeKey = Object.keys(this.flatTemplates).filter( 90 | (key) => key.indexOf(`${logicalId}.Type`) > -1 91 | )[0]; 92 | 93 | const resourceType = this.flatTemplates[typeKey]; 94 | return resourceType; 95 | } 96 | 97 | /** 98 | * 99 | * @param stackName Stack name 100 | * @param logicalId the logical ID of the Resource to find 101 | * @returns Json object of the Cloudformation resource properties 102 | */ 103 | public getResourcePropertiesFromLogicalId( 104 | stackName: string, 105 | logicalId: string 106 | ): Json { 107 | return this.templates[stackName].Resources[logicalId].Properties; 108 | } 109 | 110 | /** 111 | * Performs a Describe Stack API call with the AWS SDK to determine what 112 | * the CloudFormation Exports are for a given Stack Name. 113 | * 114 | * @param stackName the CloudFormation stack name to query against 115 | * @param region the AWS region to target 116 | * @returns CloudFormation Stack object 117 | */ 118 | private describeStack(stackName: string, region: string): Stack | undefined { 119 | let stack: Stack | undefined; 120 | try { 121 | const output = cp.spawnSync( 122 | 'aws', 123 | [ 124 | 'cloudformation', 125 | 'describe-stacks', 126 | '--stack-name', 127 | stackName, 128 | '--region', 129 | region, 130 | '--output', 131 | 'json', 132 | ], 133 | { encoding: 'utf8' } 134 | ); 135 | 136 | if (output.status !== 0) { 137 | return undefined; 138 | } 139 | 140 | const response = JSON.parse(output.stdout); 141 | const stacks: Stack[] = response.Stacks; 142 | 143 | stack = stacks[0]; 144 | } catch {} 145 | 146 | return stack; 147 | } 148 | 149 | /** 150 | * Builds an Export lookup table from the provided AWS SDK CloudFormation 151 | * stack object. This will be in the form of `{ 'MyExport': 'foobar' }`. 152 | * 153 | * @param stack 154 | * @returns 155 | */ 156 | private createExportMap(stack: Stack): FlatJson { 157 | const exports: FlatJson = {}; 158 | const outputs = stack.Outputs ?? []; 159 | for (const output of outputs) { 160 | if (output.ExportName && output.OutputValue) { 161 | exports[output.ExportName] = output.OutputValue; 162 | } 163 | } 164 | return exports; 165 | } 166 | 167 | /** 168 | * Rebuilds an `Fn::Join` statement from the Flat Json path. This is 169 | * necessary because CDK does not correctly inject values back into 170 | * `Fn::Join` blocks. The rebuilt object is injected back into the spot where 171 | * the original `Fn::Join` was at. By using this method, we can get around 172 | * the problem where `addPropertyOverride` does not work for Fn::Join lists. 173 | * 174 | * @param fnJoinFlatJsonPath 175 | * @returns string of Fn::Join (may be string or Json string depending on 176 | * if CloudFormation needs to reference values or if the entire string can 177 | * be hardcoded back) 178 | */ 179 | public rebuildFnJoin(fnJoinFlatJsonPath: string): string { 180 | // Find the items that this fnJoin references 181 | const items = Object.keys(this.flatTemplates).filter((key) => 182 | key.includes(`${fnJoinFlatJsonPath}.1`) 183 | ); 184 | 185 | // Sort the items so that they are rebuilt in the right order 186 | items.sort(); 187 | 188 | // Get the actual values from the flat templates map 189 | const listOfValues: string[] = []; 190 | items.forEach((item) => { 191 | if (Object.keys(this.fnJoins).includes(item)) { 192 | listOfValues.push(this.fnJoins[item]); 193 | } else if (item.split('.').slice(-1)[0] == 'Ref') { 194 | listOfValues.push(Fn.ref(this.flatTemplates[item])); 195 | } else if (!item.includes('Fn::GetAtt.1')) { 196 | listOfValues.push(this.flatTemplates[item]); 197 | } 198 | }); 199 | 200 | // Return the rebuilt Join statement 201 | return Fn.join(this.flatTemplates[`${fnJoinFlatJsonPath}.0`], listOfValues); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/patches/resource-extractor/flattener.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Json, FlatJson } from './types'; 6 | 7 | /** 8 | * Contains static helper functions to deal with flattening objects, 9 | * retrieving values from flat objects and setting values on flat objects. 10 | */ 11 | export class Flattener { 12 | /** 13 | * Flattens a provided Json object, moving all nested objects to the top 14 | * level and separating with dots, example: 15 | * ``` 16 | * { 17 | * "Stack.Resources.Foo.Properties.Tags.0.Value": "bar" 18 | * } 19 | * ``` 20 | * 21 | * @param obj complex object to flatten 22 | * @returns key-value pairs of object separated by dots, and the value 23 | */ 24 | public static flattenObject = (obj: Json): FlatJson => { 25 | var toReturn: { [key: string]: string } = {}; 26 | for (let i in obj) { 27 | if (!obj.hasOwnProperty(i)) continue; 28 | 29 | if (typeof obj[i] == 'object' && obj[i] !== null) { 30 | let flatObject = Flattener.flattenObject(obj[i]); 31 | for (let x in flatObject) { 32 | if (!flatObject.hasOwnProperty(x)) continue; 33 | toReturn[i + '.' + x] = flatObject[x]; 34 | } 35 | } else { 36 | toReturn[i] = obj[i]; 37 | } 38 | } 39 | return toReturn; 40 | }; 41 | 42 | /** 43 | * Retrieves a value from a provided Json object by using the provided flat 44 | * path to look it up 45 | * 46 | * @param obj object to search through 47 | * @param path string of where in the object to get, separated by dots 48 | * @returns found value 49 | */ 50 | public static getValueByPath = (obj: Json, path: string): any => 51 | path.split('.').reduce((acc, part) => (acc ? acc[part] : undefined), obj); 52 | 53 | /** 54 | * Updates a value in a provided Json object by using the provided flat 55 | * path to look it up and the provided value to set 56 | * 57 | * @param obj object to update 58 | * @param path string of path to key to update (separated by dots) 59 | * @param value value to set in object path 60 | */ 61 | public static setToValue = (obj: Json, path: string, value: any): void => { 62 | let i; 63 | const pathSplit = path.split('.'); 64 | for (i = 0; i < pathSplit.length - 1; i++) obj = obj[pathSplit[i]]; 65 | obj[pathSplit[i]] = value; 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /src/patches/resource-extractor/resourceTransformer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Aws } from 'aws-cdk-lib'; 6 | import { CfnApiKey } from 'aws-cdk-lib/aws-apigateway'; 7 | import { CfnAlarm } from 'aws-cdk-lib/aws-cloudwatch'; 8 | import { CfnTable } from 'aws-cdk-lib/aws-dynamodb'; 9 | import { CfnCluster, CfnService, CfnTaskDefinition } from 'aws-cdk-lib/aws-ecs'; 10 | import { CfnDomain as CfnDomainEss } from 'aws-cdk-lib/aws-elasticsearch'; 11 | import { CfnFunction } from 'aws-cdk-lib/aws-lambda'; 12 | import { CfnLogGroup } from 'aws-cdk-lib/aws-logs'; 13 | import { CfnDomain as CfnDomainOss } from 'aws-cdk-lib/aws-opensearchservice'; 14 | import { CfnBucket } from 'aws-cdk-lib/aws-s3'; 15 | import { CfnTopic } from 'aws-cdk-lib/aws-sns'; 16 | import { CfnQueue } from 'aws-cdk-lib/aws-sqs'; 17 | import { CfnParameter } from 'aws-cdk-lib/aws-ssm'; 18 | import { CfnStateMachine } from 'aws-cdk-lib/aws-stepfunctions'; 19 | import { CfnStore } from './cfnStore'; 20 | import { FlatJson, Json } from './types'; 21 | 22 | type Transformer = ( 23 | stackName: string, 24 | logicalId: string, 25 | resourceProperties: Json 26 | ) => string; 27 | 28 | export class MissingTransformError extends Error { 29 | constructor(resourceType: string) { 30 | super(` 31 | Unable to find transform for resource type: ${resourceType}. 32 | You can provide an additional transformation to the Aspect to resolve 33 | this and/or open a GitHub Issue to get the resource transformation 34 | added at https://github.com/cdklabs/cdk-enterprise-iac/issues. 35 | `); 36 | } 37 | } 38 | 39 | export enum ResourceTransform { 40 | STACK_NAME = 'ResourceTransform.STACK_NAME', 41 | LOGICAL_ID = 'ResourceTransform.LOGICAL_ID', 42 | } 43 | 44 | export interface ResourceTransformerProps { 45 | readonly cfnStore: CfnStore; 46 | readonly additionalTransforms?: FlatJson; 47 | } 48 | 49 | export class ResourceTransformer { 50 | private readonly cfn: CfnStore; 51 | private readonly defaultTransforms: { [key: string]: Transformer }; 52 | private readonly additionalTransforms?: FlatJson; 53 | 54 | constructor(props: ResourceTransformerProps) { 55 | this.cfn = props.cfnStore; 56 | this.defaultTransforms = this.createDefaultTransforms(); 57 | this.additionalTransforms = props.additionalTransforms; 58 | } 59 | 60 | /** 61 | * Helper function that generates the beginning portion of an AWS Arn. 62 | */ 63 | private generateArnPreamble( 64 | service: string, 65 | includeRegion: boolean = true, 66 | includeAccount: boolean = true 67 | ): string { 68 | const region = includeRegion ? `${Aws.REGION}` : ''; 69 | const account = includeAccount ? `${Aws.ACCOUNT_ID}` : ''; 70 | 71 | return `arn:${Aws.PARTITION}:${service}:${region}:${account}`; 72 | } 73 | 74 | /** 75 | * Creates the Default Transformation function table that contains transform 76 | * functions for each CloudFormation Resource Type. 77 | * 78 | * This should be updated to support additional resource types. 79 | * In addition, it may need to be fixed if some of the transformations are 80 | * found to be incorrect or inconsistent. 81 | * 82 | * @returns object in the form of { 'AWS::S3::Bucket': transform function } 83 | */ 84 | private createDefaultTransforms(): { [key: string]: Transformer } { 85 | return { 86 | /** Standard */ 87 | [CfnTable.CFN_RESOURCE_TYPE_NAME]: ( 88 | stackName, 89 | logicalId, 90 | _resourceProperties 91 | ) => { 92 | const partial = `${stackName}-${logicalId}*`; 93 | const preamble = this.generateArnPreamble('dynamodb'); 94 | return `${preamble}:table/${partial}`; 95 | }, 96 | [CfnDomainEss.CFN_RESOURCE_TYPE_NAME]: ( 97 | _, 98 | logicalId, 99 | _resourceProperties 100 | ) => { 101 | const partial = `${logicalId.substring(0, 15).toLowerCase()}*`; 102 | const preamble = this.generateArnPreamble('es'); 103 | return `${preamble}:domain/${partial}`; 104 | }, 105 | [CfnDomainOss.CFN_RESOURCE_TYPE_NAME]: ( 106 | _, 107 | logicalId, 108 | _resourceProperties 109 | ) => { 110 | const partial = `${logicalId.substring(0, 15).toLowerCase()}*`; 111 | const preamble = this.generateArnPreamble('es'); 112 | return `${preamble}:domain/${partial}`; 113 | }, 114 | [CfnTaskDefinition.CFN_RESOURCE_TYPE_NAME]: ( 115 | stackName, 116 | logicalId, 117 | _resourceProperties 118 | ) => { 119 | const partial = `${stackName}${logicalId}*:*`.replace('-', ''); 120 | const preamble = this.generateArnPreamble('ecs'); 121 | return `${preamble}:task-definition/${partial}`; 122 | }, 123 | [CfnCluster.CFN_RESOURCE_TYPE_NAME]: ( 124 | stackName, 125 | logicalId, 126 | _resourceProperties 127 | ) => { 128 | const partial = `${stackName}-${logicalId}*`; 129 | const preamble = this.generateArnPreamble('ecs'); 130 | return `${preamble}:cluster/${partial}`; 131 | }, 132 | [CfnService.CFN_RESOURCE_TYPE_NAME]: ( 133 | stackName, 134 | logicalId, 135 | _resourceProperties 136 | ) => { 137 | const partial = `*/${stackName}-${logicalId}*`; 138 | const preamble = this.generateArnPreamble('ecs'); 139 | return `${preamble}:service/${partial}`; 140 | }, 141 | /** Colon-resource name grouping */ 142 | [CfnLogGroup.CFN_RESOURCE_TYPE_NAME]: ( 143 | stackName, 144 | logicalId, 145 | _resourceProperties 146 | ) => { 147 | const partial = `${stackName}-${logicalId}*:*`; 148 | const preamble = this.generateArnPreamble('logs'); 149 | return `${preamble}:log-group:${partial}`; 150 | }, 151 | [CfnFunction.CFN_RESOURCE_TYPE_NAME]: ( 152 | stackName, 153 | logicalId, 154 | _resourceProperties 155 | ) => { 156 | const partial = `${stackName}-${logicalId}`.substring(0, 50) + '*'; 157 | const preamble = this.generateArnPreamble('lambda'); 158 | return `${preamble}:function:${partial}`; 159 | }, 160 | [CfnStateMachine.CFN_RESOURCE_TYPE_NAME]: ( 161 | _, 162 | logicalId, 163 | _resourceProperties 164 | ) => { 165 | const partial = `${logicalId}*`; 166 | const preamble = this.generateArnPreamble('states'); 167 | return `${preamble}:stateMachine:${partial}`; 168 | }, 169 | [CfnAlarm.CFN_RESOURCE_TYPE_NAME]: ( 170 | stackName, 171 | logicalId, 172 | _resourceProperties 173 | ) => { 174 | const partial = `${stackName}-${logicalId}*`; 175 | const preamble = this.generateArnPreamble('cloudwatch'); 176 | return `${preamble}:alarm:${partial}`; 177 | }, 178 | /** No resource name grouping */ 179 | [CfnQueue.CFN_RESOURCE_TYPE_NAME]: ( 180 | stackName, 181 | logicalId, 182 | _resourceProperties 183 | ) => { 184 | const partial = `${stackName}-${logicalId}*`; 185 | const preamble = this.generateArnPreamble('sqs'); 186 | return `${preamble}:${partial}`; 187 | }, 188 | [CfnBucket.CFN_RESOURCE_TYPE_NAME]: ( 189 | stackName, 190 | logicalId, 191 | resourceProperties 192 | ) => { 193 | let partial: string; 194 | if ( 195 | resourceProperties !== undefined && 196 | 'BucketName' in resourceProperties 197 | ) { 198 | partial = `${resourceProperties.BucketName}*`; 199 | } else { 200 | partial = `${stackName}-${logicalId}*`.toLowerCase(); 201 | } 202 | const preamble = this.generateArnPreamble('s3', false, false); 203 | return `${preamble}:${partial}`; 204 | }, 205 | [CfnTopic.CFN_RESOURCE_TYPE_NAME]: ( 206 | stackName, 207 | logicalId, 208 | _resourceProperties 209 | ) => { 210 | const partial = `${stackName}-${logicalId}*`; 211 | const preamble = this.generateArnPreamble('sns'); 212 | return `${preamble}:${partial}`; 213 | }, 214 | /** Non-standard */ 215 | [CfnParameter.CFN_RESOURCE_TYPE_NAME]: ( 216 | _, 217 | logicalId, 218 | resourceProperties 219 | ) => { 220 | if ('Name' in resourceProperties) { 221 | return resourceProperties.Name; 222 | } 223 | const partial = `CFN-${logicalId}*`; 224 | return partial; 225 | }, 226 | [CfnApiKey.CFN_RESOURCE_TYPE_NAME]: ( 227 | stackName, 228 | logicalId, 229 | _resourceProperties 230 | ) => { 231 | const partial = `${stackName.substring(0, 6)}-${logicalId.substring( 232 | 0, 233 | 5 234 | )}*`; 235 | return partial; 236 | }, 237 | }; 238 | } 239 | 240 | /** 241 | * Transforms resource names to partial values (primarily ARNs) using 242 | * wildcards. 243 | * 244 | * Takes in a generated resource name from CDK and transforms it to a 245 | * partial value that is used to replace resource references that use 246 | * `Fn::GetAtt` from the generated resource name. This is mainly used to 247 | * avoid cyclical dependencies within CDK and ensure that Policies can be 248 | * correctly created without knowing the future value of a resource. This 249 | * relies on the assumption that the developer does NOT input the `name` 250 | * of the resource they are creating. In other words, you must let CDK 251 | * generate the resource name. 252 | * 253 | * @param logicalId the Logical ID of the Resource generated by CDK 254 | * @returns string of partial match value to use in IAM Policies 255 | */ 256 | public toPartial(logicalId: string) { 257 | const stackName = this.cfn.getStackNameFromLogicalId(logicalId); 258 | const resourceType = this.cfn.getResourceTypeFromLogicalId(logicalId); 259 | const resourceProperties = this.cfn.getResourcePropertiesFromLogicalId( 260 | stackName, 261 | logicalId 262 | ); 263 | 264 | if ( 265 | this.additionalTransforms && 266 | resourceType in this.additionalTransforms 267 | ) { 268 | return this.additionalTransforms[resourceType] 269 | .replace(ResourceTransform.STACK_NAME, stackName) 270 | .replace(ResourceTransform.LOGICAL_ID, logicalId); 271 | } else if (resourceType in this.defaultTransforms) { 272 | const transformResourceToPartial = this.defaultTransforms[resourceType]; 273 | return transformResourceToPartial( 274 | stackName, 275 | logicalId, 276 | resourceProperties 277 | ); 278 | } else { 279 | throw new MissingTransformError(resourceType); 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/patches/resource-extractor/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | export type Json = { [key: string]: any }; 6 | export type FlatJson = { [key: string]: string }; 7 | -------------------------------------------------------------------------------- /src/patches/setApiGatewayEndpointConfiguration.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { CfnResource, IAspect } from 'aws-cdk-lib'; 6 | import { EndpointType } from 'aws-cdk-lib/aws-apigateway'; 7 | import { IConstruct } from 'constructs'; 8 | 9 | export interface SetApiGatewayEndpointConfigurationProps { 10 | /** 11 | * API Gateway endpoint type to override to. Defaults to EndpointType.REGIONAL 12 | * 13 | * @default EndpointType.REGIONAL 14 | */ 15 | readonly endpointType?: EndpointType; 16 | } 17 | 18 | /** 19 | * Override RestApis to use a set endpoint configuration. 20 | * 21 | * Some regions don't support EDGE endpoints, and some enterprises require 22 | * specific endpoint types for RestApis 23 | */ 24 | export class SetApiGatewayEndpointConfiguration implements IAspect { 25 | private _endpointType: EndpointType; 26 | 27 | constructor(props?: SetApiGatewayEndpointConfigurationProps) { 28 | this._endpointType = props?.endpointType || EndpointType.REGIONAL; 29 | } 30 | 31 | public visit(node: IConstruct): void { 32 | if (node instanceof CfnResource) { 33 | const cfnResourceNode: CfnResource = node; 34 | if (cfnResourceNode.cfnResourceType == 'AWS::ApiGateway::RestApi') { 35 | cfnResourceNode.addPropertyOverride('EndpointConfiguration.Types', [ 36 | this._endpointType.toString(), 37 | ]); 38 | cfnResourceNode.addPropertyOverride( 39 | 'Parameters.endpointConfigurationTypes', 40 | this._endpointType.toString() 41 | ); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | export const getResourceId = (resourcePath: string): string => { 6 | return resourcePath.replace(/[/:]/g, '-'); 7 | }; 8 | -------------------------------------------------------------------------------- /test/constructs/ecsIsoServiceAutoscaler/ecsIsoServiceAutoscaler.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { spawnSync } from 'child_process'; 6 | import * as path from 'path'; 7 | import { CfnElement, Duration, Stack } from 'aws-cdk-lib'; 8 | import { Match, Template } from 'aws-cdk-lib/assertions'; 9 | import { Alarm } from 'aws-cdk-lib/aws-cloudwatch'; 10 | import { 11 | CfnCluster, 12 | CfnService, 13 | Cluster, 14 | ContainerImage, 15 | FargateService, 16 | FargateTaskDefinition, 17 | } from 'aws-cdk-lib/aws-ecs'; 18 | import { CfnRole, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; 19 | import { IConstruct } from 'constructs'; 20 | import { EcsIsoServiceAutoscaler } from '../../../src/constructs/ecsIsoServiceAutoscaler/ecsIsoServiceAutoscaler'; 21 | 22 | let stack: Stack; 23 | 24 | describe('Python Lambda function tests', () => { 25 | test('lambda python pytest', () => { 26 | const result = spawnSync(path.join(__dirname, 'resources', 'test.sh'), { 27 | stdio: 'inherit', 28 | }); 29 | expect(result.status).toBe(0); 30 | }); 31 | }); 32 | 33 | describe('EcsIsoServiceAutoscaler construct', () => { 34 | let cluster: Cluster; 35 | let service: FargateService; 36 | let alarm: Alarm; 37 | let role: Role; 38 | beforeEach(() => { 39 | stack = new Stack(); 40 | cluster = new Cluster(stack, 'TestCluster'); 41 | const taskDefinition = new FargateTaskDefinition( 42 | stack, 43 | 'TestTaskDefinition' 44 | ); 45 | taskDefinition.addContainer('SomeContainer', { 46 | image: ContainerImage.fromRegistry( 47 | 'public.ecr.aws/ecs-sample-image/amazon-ecs-sample:latest' 48 | ), 49 | }); 50 | service = new FargateService(stack, 'TestService', { 51 | cluster, 52 | taskDefinition, 53 | desiredCount: 5, 54 | }); 55 | role = new Role(stack, 'TestTaskRole', { 56 | assumedBy: new ServicePrincipal('lambda.amazonaws.com'), 57 | }); 58 | alarm = new Alarm(stack, 'TestAlarm', { 59 | metric: cluster.metricCpuUtilization(), 60 | threshold: 10, 61 | evaluationPeriods: 2, 62 | }); 63 | }); 64 | 65 | test('Expected number of resources are created with defaults', () => { 66 | new EcsIsoServiceAutoscaler(stack, 'TestEcsIsoServiceAutoscaler', { 67 | ecsCluster: cluster, 68 | ecsService: service, 69 | scaleAlarm: alarm, 70 | role, 71 | }); 72 | 73 | const template = Template.fromStack(stack); 74 | 75 | // Lambda with default Env vars 76 | template.resourcePropertiesCountIs( 77 | 'AWS::Lambda::Function', 78 | { 79 | Environment: { 80 | Variables: Match.objectLike({ 81 | MINIMUM_TASK_COUNT: '1', 82 | MAXIMUM_TASK_COUNT: '10', 83 | SCALE_OUT_INCREMENT: '1', 84 | SCALE_OUT_COOLDOWN: '60', 85 | SCALE_IN_INCREMENT: '1', 86 | SCALE_IN_COOLDOWN: '60', 87 | }), 88 | }, 89 | }, 90 | 1 91 | ); 92 | }); 93 | test('Autoscaling props provided', () => { 94 | new EcsIsoServiceAutoscaler(stack, 'TestEcsIsoServiceAutoscaler', { 95 | ecsCluster: cluster, 96 | ecsService: service, 97 | scaleAlarm: alarm, 98 | role, 99 | minimumTaskCount: 5, 100 | maximumTaskCount: 15, 101 | scaleInIncrement: 2, 102 | scaleOutIncrement: 2, 103 | scaleInCooldown: Duration.seconds(120), 104 | scaleOutCooldown: Duration.seconds(120), 105 | }); 106 | 107 | const template = Template.fromStack(stack); 108 | 109 | // Lambda with default Env vars 110 | template.resourcePropertiesCountIs( 111 | 'AWS::Lambda::Function', 112 | { 113 | Environment: { 114 | Variables: Match.objectLike({ 115 | MINIMUM_TASK_COUNT: '5', 116 | MAXIMUM_TASK_COUNT: '15', 117 | SCALE_OUT_INCREMENT: '2', 118 | SCALE_OUT_COOLDOWN: '120', 119 | SCALE_IN_INCREMENT: '2', 120 | SCALE_IN_COOLDOWN: '120', 121 | }), 122 | }, 123 | }, 124 | 1 125 | ); 126 | }); 127 | test('Lambda creates own role if optional role not passed, and role has least privilege', () => { 128 | const autoScaler = new EcsIsoServiceAutoscaler( 129 | stack, 130 | 'TestEcsIsoServiceAutoscaler', 131 | { 132 | ecsCluster: cluster, 133 | ecsService: service, 134 | scaleAlarm: alarm, 135 | } 136 | ); 137 | 138 | const autoScalerRole = autoScaler.node.findAll().find((x: IConstruct) => { 139 | return x instanceof CfnRole; 140 | }); 141 | const serviceId = service.node.findAll().find((x: IConstruct) => { 142 | return x instanceof CfnService; 143 | }); 144 | 145 | const clusterId = cluster.node.findAll().find((x: IConstruct) => { 146 | return x instanceof CfnCluster; 147 | }); 148 | 149 | const template = Template.fromStack(stack); 150 | 151 | template.resourcePropertiesCountIs( 152 | 'AWS::IAM::Policy', 153 | { 154 | PolicyDocument: { 155 | Statement: Match.arrayWith([ 156 | Match.objectLike({ 157 | Action: 'cloudwatch:DescribeAlarms', 158 | Resource: '*', 159 | }), 160 | Match.objectLike({ 161 | Action: ['ecs:DescribeServices', 'ecs:UpdateService'], 162 | Resource: { 163 | Ref: stack.getLogicalId(serviceId as CfnElement).toString(), 164 | }, 165 | Condition: { 166 | StringLike: { 167 | 'ecs:cluster': { 168 | 'Fn::GetAtt': [ 169 | stack.getLogicalId(clusterId as CfnElement).toString(), 170 | 'Arn', 171 | ], 172 | }, 173 | }, 174 | }, 175 | }), 176 | ]), 177 | }, 178 | Roles: [ 179 | { 180 | Ref: stack.getLogicalId(autoScalerRole as CfnElement).toString(), 181 | }, 182 | ], 183 | }, 184 | 1 185 | ); 186 | }); 187 | test('Autoscaler Lambda is accessible as a property', () => { 188 | const autoScaler = new EcsIsoServiceAutoscaler( 189 | stack, 190 | 'TestEcsIsoServiceAutoscaler', 191 | { 192 | ecsCluster: cluster, 193 | ecsService: service, 194 | scaleAlarm: alarm, 195 | } 196 | ); 197 | 198 | expect(autoScaler).toHaveProperty('ecsScalingManagerFunction'); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /test/constructs/ecsIsoServiceAutoscaler/resources/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/python:3.11 2 | 3 | # add everything to /opt/awscli (this is where `aws` is executed from) 4 | ADD . ${LAMBDA_TASK_ROOT} 5 | 6 | # install boto3, which is available on Lambda, and pytest 7 | RUN pip3 install boto3 pytest 8 | 9 | # run tests 10 | WORKDIR ${LAMBDA_TASK_ROOT} 11 | RUN ["pytest"] 12 | 13 | ENTRYPOINT [ "/bin/bash" ] 14 | 15 | -------------------------------------------------------------------------------- /test/constructs/ecsIsoServiceAutoscaler/resources/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # SPDX-License-Identifier: Apache-2.0 5 | #--------------------------------------------------------------------------------------------------- 6 | # exeuctes unit tests 7 | # 8 | # prepares a staging directory with the requirements 9 | set -e 10 | scriptdir=$(cd $(dirname $0) && pwd) 11 | 12 | rm -fr ${scriptdir}/__pycache__ 13 | 14 | # prepare staging directory 15 | staging=$(mktemp -d) 16 | mkdir -p ${staging} 17 | cd ${staging} 18 | 19 | # copy src and overlay with test 20 | cp -f ${scriptdir}/../../../../resources/constructs/ecsIsoServiceAutoscaler/* $PWD 21 | cp -f ${scriptdir}/* $PWD 22 | 23 | # this will run our tests inside the right environment 24 | docker build . -------------------------------------------------------------------------------- /test/constructs/ecsIsoServiceAutoscaler/resources/test_ecs_scaling_manager.py: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # SPDX-License-Identifier: Apache-2.0 3 | from typing import Any, Dict 4 | from unittest.mock import patch 5 | 6 | import pytest 7 | 8 | with patch("boto3.client"): 9 | from ecs_scaling_manager import ( #type: ignore 10 | _get_alarm_states, 11 | _get_ecs_service, 12 | _get_time_since_last_ecs_update, 13 | _trigger_scaling_action, 14 | ) 15 | 16 | def test_get_alarm_states(boto3_cw_alarm_not_ok_response): 17 | """ 18 | Tests getting alarm state data from a mocked boto3 response. The expected 19 | response should be a list in the format of ["OK", "ALARM"]. 20 | """ 21 | with patch("ecs_scaling_manager.cw_client.describe_alarms") as mock: 22 | mock.return_value = boto3_cw_alarm_not_ok_response 23 | alarm_states = _get_alarm_states(alarm_name="Alarm") 24 | 25 | assert mock.called 26 | assert isinstance(alarm_states, list) 27 | assert "OK" in alarm_states 28 | assert "ALARM" not in alarm_states 29 | 30 | 31 | def test_get_alarm_states(boto3_cw_alarm_ok_response): 32 | """ 33 | Tests getting alarm state data from a mocked boto3 response. The expected 34 | response should be a list in the format of ["OK", "ALARM"]. 35 | """ 36 | with patch("ecs_scaling_manager.cw_client.describe_alarms") as mock: 37 | mock.return_value = boto3_cw_alarm_ok_response 38 | alarm_states = _get_alarm_states(alarm_name="Alarm") 39 | 40 | assert mock.called 41 | assert isinstance(alarm_states, list) 42 | assert "OK" in alarm_states 43 | assert "ALARM" not in alarm_states 44 | 45 | 46 | def test_get_ecs_service(boto3_ecs_service_response): 47 | """ 48 | Tests getting ECS Service data from a mocked boto3 response. The expected 49 | response should be in a dictionary format {...} that contains the single 50 | ECS service. 51 | """ 52 | with patch("ecs_scaling_manager.ecs_client.describe_services") as mock: 53 | mock.return_value = boto3_ecs_service_response 54 | service = _get_ecs_service( 55 | cluster_name="Cluster", service_name="Service" 56 | ) 57 | 58 | assert mock.called 59 | assert isinstance(service, dict) 60 | assert "deployments" in service 61 | 62 | 63 | @pytest.mark.parametrize('boto3_ecs_service_response', [180], indirect=True) 64 | def test_get_time_since_last_ecs_update(boto3_ecs_service_response): 65 | """ 66 | Tests getting the time since the ECS Service was last updated by pulling 67 | the value out of the ECS Service response data, which is mocked. 68 | """ 69 | with patch("ecs_scaling_manager.ecs_client.describe_services") as mock: 70 | mock.return_value = boto3_ecs_service_response 71 | service = _get_ecs_service( 72 | cluster_name="Cluster", service_name="Service" 73 | ) 74 | 75 | time_since = _get_time_since_last_ecs_update(service) 76 | 77 | assert mock.called == True 78 | assert isinstance(time_since, float) 79 | assert int(time_since) > 178 and int(time_since) < 182 80 | 81 | 82 | def test_trigger_scaling_action_out_outside_cooldown(): 83 | """ 84 | Tests logic to perform an ECS Scaling OUT Action outside of the cooldown 85 | window, which should correctly call the ECS Service to update the service. 86 | """ 87 | with patch("ecs_scaling_manager.ecs_client.update_service") as mock: 88 | _trigger_scaling_action( 89 | type_="OUT", 90 | increment=8, 91 | current_count=3, 92 | end_count=50, 93 | cooldown=300, 94 | last_update=301 95 | ) 96 | call_args: Dict[str, Any] = mock.call_args[1] 97 | 98 | assert mock.called == True 99 | assert call_args.get("desiredCount") == 11 100 | 101 | 102 | def test_trigger_scaling_action_out_inside_cooldown(): 103 | """ 104 | Tests logic to perform an ECS Scaling OUT Action inside of the cooldown 105 | window, which should not actually scale out since we are still in the 106 | cooldown period. 107 | """ 108 | with patch("ecs_scaling_manager.ecs_client.update_service") as mock: 109 | _trigger_scaling_action( 110 | type_="out", 111 | increment=8, 112 | current_count=3, 113 | end_count=50, 114 | cooldown=300, 115 | last_update=250 116 | ) 117 | 118 | assert mock.called == False 119 | 120 | 121 | def test_trigger_scaling_action_in_outside_cooldown(): 122 | """ 123 | Tests logic to perform an ECS Scaling IN Action outside of the cooldown 124 | window, which should correctly call the ECS Service to update the service. 125 | """ 126 | with patch("ecs_scaling_manager.ecs_client.update_service") as mock: 127 | _trigger_scaling_action( 128 | type_="IN", 129 | increment=8, 130 | current_count=20, 131 | end_count=3, 132 | cooldown=300, 133 | last_update=301 134 | ) 135 | call_args: Dict[str, Any] = mock.call_args[1] 136 | 137 | assert mock.called == True 138 | assert call_args.get("desiredCount") == 12 139 | 140 | 141 | def test_trigger_scaling_action_in_inside_cooldown(): 142 | """ 143 | Tests logic to perform an ECS Scaling IN Action inside of the cooldown 144 | window, which should not actually scale in since we are still in the 145 | cooldown period. 146 | """ 147 | with patch("ecs_scaling_manager.ecs_client.update_service") as mock: 148 | _trigger_scaling_action( 149 | type_="in", 150 | increment=8, 151 | current_count=20, 152 | end_count=3, 153 | cooldown=300, 154 | last_update=250 155 | ) 156 | 157 | assert mock.called == False 158 | 159 | 160 | def test_trigger_scaling_action_in_to_limit(): 161 | """ 162 | Tests logic to scale in to the limit defined. The function should see that 163 | the end count is at the limit and perform the correct math to only scale in 164 | to the end_count defined and not beyond that. 165 | """ 166 | with patch("ecs_scaling_manager.ecs_client.update_service") as mock: 167 | _trigger_scaling_action( 168 | type_="in", 169 | increment=8, 170 | current_count=8, 171 | end_count=3, 172 | cooldown=300, 173 | last_update=301 174 | ) 175 | call_args: Dict[str, Any] = mock.call_args[1] 176 | 177 | assert mock.called == True 178 | assert call_args.get("desiredCount") == 3 179 | 180 | 181 | def test_trigger_scaling_action_out_to_limit(): 182 | """ 183 | Tests logic to scale out to the limit defined. The function should see that 184 | the end count is at the limit and perform the correct math to only scale 185 | out to the end_count defined and not beyond that. 186 | """ 187 | with patch("ecs_scaling_manager.ecs_client.update_service") as mock: 188 | _trigger_scaling_action( 189 | type_="Out", 190 | increment=8, 191 | current_count=45, 192 | end_count=50, 193 | cooldown=300, 194 | last_update=301 195 | ) 196 | call_args: Dict[str, Any] = mock.call_args[1] 197 | 198 | assert mock.called == True 199 | assert call_args.get("desiredCount") == 50 -------------------------------------------------------------------------------- /test/constructs/enterpriseDnsResolver/enterpriseDnsResolver.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Stack } from 'aws-cdk-lib'; 6 | import { Match, Template } from 'aws-cdk-lib/assertions'; 7 | import { Vpc } from 'aws-cdk-lib/aws-ec2'; 8 | 9 | import { EnterpriseDnsResolver } from '../../../src/constructs/enterpriseDnsResolver/enterpriseDnsResolver'; 10 | 11 | let stack: Stack; 12 | 13 | describe('Enterprise DNS resolver', () => { 14 | stack = new Stack(); 15 | const enterpriseDnsIpAddresses = ['1.1.1.1', '2.2.2.2', '3.3.3.3', '4.4.4.4']; 16 | const vpc = new Vpc(stack, 'TestVpc', { maxAzs: 2 }); 17 | 18 | new EnterpriseDnsResolver(stack, 'EnterpriseDnsResolver', { 19 | vpc, 20 | enterpriseDnsIpAddresses, 21 | }); 22 | 23 | const template = Template.fromStack(stack); 24 | 25 | test('Security group created in the right vpc', () => { 26 | template.resourcePropertiesCountIs( 27 | 'AWS::EC2::SecurityGroup', 28 | { 29 | VpcId: { 30 | Ref: Match.anyValue(), 31 | }, 32 | }, 33 | 1 34 | ); 35 | }); 36 | test('Resolver endpoint is created in all vpc subnets', () => { 37 | template.resourcePropertiesCountIs( 38 | 'AWS::Route53Resolver::ResolverEndpoint', 39 | { 40 | IpAddresses: [ 41 | // 4 subnets 42 | { 43 | SubnetId: { 44 | Ref: Match.anyValue(), 45 | }, 46 | }, 47 | { 48 | SubnetId: { 49 | Ref: Match.anyValue(), 50 | }, 51 | }, 52 | { 53 | SubnetId: { 54 | Ref: Match.anyValue(), 55 | }, 56 | }, 57 | { 58 | SubnetId: { 59 | Ref: Match.anyValue(), 60 | }, 61 | }, 62 | ], 63 | }, 64 | 1 65 | ); 66 | }); 67 | 68 | test('Two system rules are created', () => { 69 | template.resourceCountIs('AWS::Route53Resolver::ResolverRule', 2); 70 | }); 71 | 72 | test('Enterprise Dns Rule includes all provided dns IPs', () => { 73 | let targetIpMatch: any[] = []; 74 | 75 | for (let i = 0; i < enterpriseDnsIpAddresses.length; i++) { 76 | targetIpMatch.push(Match.anyValue()); 77 | } 78 | 79 | template.resourcePropertiesCountIs( 80 | 'AWS::Route53Resolver::ResolverRule', 81 | { 82 | DomainName: '.', 83 | TargetIps: [ 84 | { 85 | Ip: enterpriseDnsIpAddresses[0], 86 | }, 87 | { 88 | Ip: enterpriseDnsIpAddresses[1], 89 | }, 90 | { 91 | Ip: enterpriseDnsIpAddresses[2], 92 | }, 93 | { 94 | Ip: enterpriseDnsIpAddresses[3], 95 | }, 96 | ], 97 | RuleType: 'FORWARD', 98 | }, 99 | 1 100 | ); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /test/constructs/populateProvidedVpc/populateProvidedVpc.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Stack } from 'aws-cdk-lib'; 6 | import { Match, Template } from 'aws-cdk-lib/assertions'; 7 | import { SubnetType } from 'aws-cdk-lib/aws-ec2'; 8 | import { 9 | PopulateWithConfig, 10 | SplitVpcEvenly, 11 | SubnetConfig, 12 | } from '../../../src/constructs/populateProvidedVpc/populateProvidedVpc'; 13 | 14 | let stack: Stack; 15 | const privateRouteTableId = 'tgw-123456abcdefg'; 16 | const localRouteTableId = 'rt-abcdefg123456'; 17 | const vpcId = 'vpc-123456abcdefghijklmnop'; 18 | 19 | beforeEach(() => { 20 | stack = new Stack(); 21 | }); 22 | 23 | describe('CDKify a provided vpc', () => { 24 | test('provided manual subnet configuration applied', () => { 25 | const subnetConfig: SubnetConfig[] = [ 26 | { 27 | groupName: 'app', 28 | cidrRange: '172.16.0.0/27', 29 | availabilityZone: 'a', 30 | subnetType: SubnetType.PUBLIC, 31 | }, 32 | { 33 | groupName: 'app', 34 | cidrRange: '172.16.0.32/27', 35 | availabilityZone: 'b', 36 | subnetType: SubnetType.PUBLIC, 37 | }, 38 | { 39 | groupName: 'db', 40 | cidrRange: '172.16.0.64/27', 41 | availabilityZone: 'a', 42 | subnetType: SubnetType.PRIVATE_WITH_EGRESS, 43 | }, 44 | { 45 | groupName: 'db', 46 | cidrRange: '172.16.0.96/27', 47 | availabilityZone: 'b', 48 | subnetType: SubnetType.PRIVATE_WITH_EGRESS, 49 | }, 50 | { 51 | groupName: 'iso', 52 | cidrRange: '172.16.0.128/26', 53 | availabilityZone: 'a', 54 | subnetType: SubnetType.PRIVATE_ISOLATED, 55 | }, 56 | { 57 | groupName: 'iso', 58 | cidrRange: '172.16.0.196/26', 59 | availabilityZone: 'b', 60 | subnetType: SubnetType.PRIVATE_ISOLATED, 61 | }, 62 | ]; 63 | new PopulateWithConfig(stack, 'CDKifyVpc', { 64 | vpcId, 65 | privateRouteTableId, 66 | localRouteTableId, 67 | subnetConfig, 68 | }); 69 | const template = Template.fromStack(stack); 70 | template.resourceCountIs('AWS::EC2::Subnet', 6); 71 | 72 | template.resourcePropertiesCountIs( 73 | 'AWS::EC2::Subnet', 74 | { 75 | Tags: Match.arrayWith([ 76 | { Key: 'aws-cdk:subnet-type', Value: 'Public' }, 77 | ]), 78 | }, 79 | 2 80 | ); 81 | template.resourcePropertiesCountIs( 82 | 'AWS::EC2::Subnet', 83 | { 84 | Tags: Match.arrayWith([ 85 | { Key: 'aws-cdk:subnet-type', Value: 'Private' }, 86 | ]), 87 | }, 88 | 2 89 | ); 90 | template.resourcePropertiesCountIs( 91 | 'AWS::EC2::Subnet', 92 | { 93 | Tags: Match.arrayWith([ 94 | { Key: 'aws-cdk:subnet-type', Value: 'Isolated' }, 95 | ]), 96 | }, 97 | 2 98 | ); 99 | template.resourcePropertiesCountIs( 100 | 'AWS::EC2::SubnetRouteTableAssociation', 101 | { 102 | RouteTableId: privateRouteTableId, 103 | }, 104 | 4 105 | ); 106 | template.resourcePropertiesCountIs( 107 | 'AWS::EC2::SubnetRouteTableAssociation', 108 | { 109 | RouteTableId: localRouteTableId, 110 | }, 111 | 2 112 | ); 113 | }); 114 | test('split vpc evenly with defaults', () => { 115 | new SplitVpcEvenly(stack, 'CDKifyVpc', { 116 | vpcId, 117 | routeTableId: privateRouteTableId, 118 | vpcCidr: '172.16.0.0/24', 119 | }); 120 | const template = Template.fromStack(stack); 121 | 122 | template.resourcePropertiesCountIs( 123 | 'AWS::EC2::Subnet', 124 | { 125 | Tags: Match.arrayWith([ 126 | { Key: 'aws-cdk:subnet-type', Value: 'Private' }, 127 | ]), 128 | }, 129 | 3 130 | ); 131 | template.resourcePropertiesCountIs( 132 | 'AWS::EC2::SubnetRouteTableAssociation', 133 | { 134 | RouteTableId: privateRouteTableId, 135 | }, 136 | 3 137 | ); 138 | }); 139 | test('split vpc evenly larger subnet and set cidrBits', () => { 140 | new SplitVpcEvenly(stack, 'CDKifyVpc', { 141 | vpcId, 142 | routeTableId: privateRouteTableId, 143 | vpcCidr: '172.16.0.0/16', 144 | cidrBits: '10', 145 | }); 146 | const template = Template.fromStack(stack); 147 | 148 | template.resourcePropertiesCountIs( 149 | 'AWS::EC2::Subnet', 150 | { 151 | CidrBlock: { 152 | 'Fn::Select': Match.arrayWith([ 153 | { 'Fn::Cidr': ['172.16.0.0/16', 3, '10'] }, 154 | ]), 155 | }, 156 | }, 157 | 3 158 | ); 159 | }); 160 | 161 | test('split vpc evenly reduced AZs', () => { 162 | new SplitVpcEvenly(stack, 'CDKifyVpc', { 163 | vpcId, 164 | routeTableId: privateRouteTableId, 165 | vpcCidr: '172.16.0.0/16', 166 | cidrBits: '10', 167 | numberOfAzs: 2, 168 | }); 169 | const template = Template.fromStack(stack); 170 | 171 | template.resourcePropertiesCountIs( 172 | 'AWS::EC2::Subnet', 173 | { 174 | CidrBlock: { 175 | 'Fn::Select': Match.arrayWith([ 176 | { 'Fn::Cidr': ['172.16.0.0/16', 2, '10'] }, 177 | ]), 178 | }, 179 | }, 180 | 2 181 | ); 182 | }); 183 | test('split vpc evenly and set subnet tag', () => { 184 | new SplitVpcEvenly(stack, 'CDKifyVpc', { 185 | vpcId, 186 | routeTableId: privateRouteTableId, 187 | vpcCidr: '172.16.0.0/24', 188 | subnetType: SubnetType.PRIVATE_ISOLATED, 189 | }); 190 | const template = Template.fromStack(stack); 191 | 192 | template.resourcePropertiesCountIs( 193 | 'AWS::EC2::Subnet', 194 | { 195 | Tags: Match.arrayWith([ 196 | { Key: 'aws-cdk:subnet-type', Value: 'Isolated' }, 197 | ]), 198 | }, 199 | 3 200 | ); 201 | }); 202 | test('error is thrown if invalid number of AZs is provided', () => { 203 | expect(() => { 204 | new SplitVpcEvenly(stack, 'CDKifyVpc7Azs', { 205 | vpcId, 206 | routeTableId: privateRouteTableId, 207 | vpcCidr: '172.16.0.0/24', 208 | numberOfAzs: 7, 209 | }); 210 | }).toThrowError('numberOfAzs must be between 2 and 6'); 211 | 212 | expect(() => { 213 | new SplitVpcEvenly(stack, 'CDKifyVpc1Az', { 214 | vpcId, 215 | routeTableId: privateRouteTableId, 216 | vpcCidr: '172.16.0.0/24', 217 | numberOfAzs: 1, 218 | }); 219 | }).toThrowError('numberOfAzs must be between 2 and 6'); 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /test/integ/patches/integ.addCfnInitProxy.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { IntegTest } from '@aws-cdk/integ-tests-alpha'; 6 | import { App, Aspects, Duration, SecretValue, Stack } from 'aws-cdk-lib'; 7 | import { 8 | CloudFormationInit, 9 | InitPackage, 10 | Instance, 11 | InstanceClass, 12 | InstanceSize, 13 | InstanceType, 14 | MachineImage, 15 | Vpc, 16 | } from 'aws-cdk-lib/aws-ec2'; 17 | import { Secret } from 'aws-cdk-lib/aws-secretsmanager'; 18 | import { AddCfnInitProxy } from '../../../src/patches/addCfnInitProxy'; 19 | 20 | const app = new App(); 21 | const stack = new Stack(app, 'integ-addCfnInitProxy-stack'); 22 | const vpc = new Vpc(stack, 'TestVpc'); 23 | new Instance(stack, 'TestInstance', { 24 | machineImage: MachineImage.latestAmazonLinux(), 25 | instanceType: InstanceType.of(InstanceClass.MEMORY5, InstanceSize.LARGE), 26 | vpc, 27 | init: CloudFormationInit.fromElements(InitPackage.yum('python3')), 28 | initOptions: { 29 | timeout: Duration.minutes(2), 30 | ignoreFailures: true, // Ignoring due to fake proxy not being real 31 | }, 32 | }); 33 | const secret = new Secret(stack, 'TestSecret', { 34 | secretObjectValue: { 35 | user: SecretValue.unsafePlainText('someUser'), 36 | password: SecretValue.unsafePlainText('superSecret123'), 37 | }, 38 | }); 39 | Aspects.of(stack).add( 40 | new AddCfnInitProxy({ 41 | proxyHost: 'example.com', 42 | proxyPort: 8080, 43 | proxyCredentials: secret, 44 | }) 45 | ); 46 | 47 | new IntegTest(app, 'IntegTest', { 48 | testCases: [stack], 49 | regions: ['us-east-1'], 50 | }); 51 | -------------------------------------------------------------------------------- /test/integ/patches/integ.addCfnInitProxy.ts.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "34.0.0", 3 | "files": { 4 | "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { 5 | "source": { 6 | "path": "IntegTestDefaultTestDeployAssertE3E7D2A4.template.json", 7 | "packaging": "file" 8 | }, 9 | "destinations": { 10 | "current_account-current_region": { 11 | "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", 12 | "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", 13 | "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" 14 | } 15 | } 16 | } 17 | }, 18 | "dockerImages": {} 19 | } -------------------------------------------------------------------------------- /test/integ/patches/integ.addCfnInitProxy.ts.snapshot/IntegTestDefaultTestDeployAssertE3E7D2A4.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "Parameters": { 3 | "BootstrapVersion": { 4 | "Type": "AWS::SSM::Parameter::Value", 5 | "Default": "/cdk-bootstrap/hnb659fds/version", 6 | "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" 7 | } 8 | }, 9 | "Rules": { 10 | "CheckBootstrapVersion": { 11 | "Assertions": [ 12 | { 13 | "Assert": { 14 | "Fn::Not": [ 15 | { 16 | "Fn::Contains": [ 17 | [ 18 | "1", 19 | "2", 20 | "3", 21 | "4", 22 | "5" 23 | ], 24 | { 25 | "Ref": "BootstrapVersion" 26 | } 27 | ] 28 | } 29 | ] 30 | }, 31 | "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." 32 | } 33 | ] 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /test/integ/patches/integ.addCfnInitProxy.ts.snapshot/cdk.out: -------------------------------------------------------------------------------- 1 | {"version":"34.0.0"} -------------------------------------------------------------------------------- /test/integ/patches/integ.addCfnInitProxy.ts.snapshot/integ-addCfnInitProxy-stack.assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "34.0.0", 3 | "files": { 4 | "7f18a11296f35510ee16538afec983ed6312e12afbf81b777089a9f8e34e2474": { 5 | "source": { 6 | "path": "asset.7f18a11296f35510ee16538afec983ed6312e12afbf81b777089a9f8e34e2474", 7 | "packaging": "zip" 8 | }, 9 | "destinations": { 10 | "current_account-current_region": { 11 | "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", 12 | "objectKey": "7f18a11296f35510ee16538afec983ed6312e12afbf81b777089a9f8e34e2474.zip", 13 | "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" 14 | } 15 | } 16 | }, 17 | "f6ae0bd531df8cb0df3161a47a37cad715818650b13382f61136aead2fcc65a6": { 18 | "source": { 19 | "path": "integ-addCfnInitProxy-stack.template.json", 20 | "packaging": "file" 21 | }, 22 | "destinations": { 23 | "current_account-current_region": { 24 | "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", 25 | "objectKey": "f6ae0bd531df8cb0df3161a47a37cad715818650b13382f61136aead2fcc65a6.json", 26 | "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" 27 | } 28 | } 29 | } 30 | }, 31 | "dockerImages": {} 32 | } -------------------------------------------------------------------------------- /test/integ/patches/integ.addCfnInitProxy.ts.snapshot/integ.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "34.0.0", 3 | "testCases": { 4 | "IntegTest/DefaultTest": { 5 | "stacks": [ 6 | "integ-addCfnInitProxy-stack" 7 | ], 8 | "regions": [ 9 | "us-east-1" 10 | ], 11 | "assertionStack": "IntegTest/DefaultTest/DeployAssert", 12 | "assertionStackName": "IntegTestDefaultTestDeployAssertE3E7D2A4" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /test/integ/patches/integ.addCfnInitProxy.ts.snapshot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "34.0.0", 3 | "artifacts": { 4 | "integ-addCfnInitProxy-stack.assets": { 5 | "type": "cdk:asset-manifest", 6 | "properties": { 7 | "file": "integ-addCfnInitProxy-stack.assets.json", 8 | "requiresBootstrapStackVersion": 6, 9 | "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" 10 | } 11 | }, 12 | "integ-addCfnInitProxy-stack": { 13 | "type": "aws:cloudformation:stack", 14 | "environment": "aws://unknown-account/unknown-region", 15 | "properties": { 16 | "templateFile": "integ-addCfnInitProxy-stack.template.json", 17 | "terminationProtection": false, 18 | "validateOnSynth": false, 19 | "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", 20 | "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", 21 | "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/f6ae0bd531df8cb0df3161a47a37cad715818650b13382f61136aead2fcc65a6.json", 22 | "requiresBootstrapStackVersion": 6, 23 | "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", 24 | "additionalDependencies": [ 25 | "integ-addCfnInitProxy-stack.assets" 26 | ], 27 | "lookupRole": { 28 | "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", 29 | "requiresBootstrapStackVersion": 8, 30 | "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" 31 | } 32 | }, 33 | "dependencies": [ 34 | "integ-addCfnInitProxy-stack.assets" 35 | ], 36 | "metadata": { 37 | "/integ-addCfnInitProxy-stack/TestVpc/Resource": [ 38 | { 39 | "type": "aws:cdk:logicalId", 40 | "data": "TestVpcE77CE678" 41 | } 42 | ], 43 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet1/Subnet": [ 44 | { 45 | "type": "aws:cdk:logicalId", 46 | "data": "TestVpcPublicSubnet1SubnetA7DB1EDF" 47 | } 48 | ], 49 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet1/RouteTable": [ 50 | { 51 | "type": "aws:cdk:logicalId", 52 | "data": "TestVpcPublicSubnet1RouteTable4CBFF871" 53 | } 54 | ], 55 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet1/RouteTableAssociation": [ 56 | { 57 | "type": "aws:cdk:logicalId", 58 | "data": "TestVpcPublicSubnet1RouteTableAssociation7D1DECD9" 59 | } 60 | ], 61 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet1/DefaultRoute": [ 62 | { 63 | "type": "aws:cdk:logicalId", 64 | "data": "TestVpcPublicSubnet1DefaultRoute6C0F0315" 65 | } 66 | ], 67 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet1/EIP": [ 68 | { 69 | "type": "aws:cdk:logicalId", 70 | "data": "TestVpcPublicSubnet1EIP4884338C" 71 | } 72 | ], 73 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet1/NATGateway": [ 74 | { 75 | "type": "aws:cdk:logicalId", 76 | "data": "TestVpcPublicSubnet1NATGatewayA323E3EC" 77 | } 78 | ], 79 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet2/Subnet": [ 80 | { 81 | "type": "aws:cdk:logicalId", 82 | "data": "TestVpcPublicSubnet2Subnet80A14523" 83 | } 84 | ], 85 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet2/RouteTable": [ 86 | { 87 | "type": "aws:cdk:logicalId", 88 | "data": "TestVpcPublicSubnet2RouteTable75B88314" 89 | } 90 | ], 91 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet2/RouteTableAssociation": [ 92 | { 93 | "type": "aws:cdk:logicalId", 94 | "data": "TestVpcPublicSubnet2RouteTableAssociationB386A819" 95 | } 96 | ], 97 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet2/DefaultRoute": [ 98 | { 99 | "type": "aws:cdk:logicalId", 100 | "data": "TestVpcPublicSubnet2DefaultRoute054DAE0A" 101 | } 102 | ], 103 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet2/EIP": [ 104 | { 105 | "type": "aws:cdk:logicalId", 106 | "data": "TestVpcPublicSubnet2EIP83F7944C" 107 | } 108 | ], 109 | "/integ-addCfnInitProxy-stack/TestVpc/PublicSubnet2/NATGateway": [ 110 | { 111 | "type": "aws:cdk:logicalId", 112 | "data": "TestVpcPublicSubnet2NATGatewayA9858C31" 113 | } 114 | ], 115 | "/integ-addCfnInitProxy-stack/TestVpc/PrivateSubnet1/Subnet": [ 116 | { 117 | "type": "aws:cdk:logicalId", 118 | "data": "TestVpcPrivateSubnet1SubnetCC65D771" 119 | } 120 | ], 121 | "/integ-addCfnInitProxy-stack/TestVpc/PrivateSubnet1/RouteTable": [ 122 | { 123 | "type": "aws:cdk:logicalId", 124 | "data": "TestVpcPrivateSubnet1RouteTable469B0105" 125 | } 126 | ], 127 | "/integ-addCfnInitProxy-stack/TestVpc/PrivateSubnet1/RouteTableAssociation": [ 128 | { 129 | "type": "aws:cdk:logicalId", 130 | "data": "TestVpcPrivateSubnet1RouteTableAssociationFFD4DFF7" 131 | } 132 | ], 133 | "/integ-addCfnInitProxy-stack/TestVpc/PrivateSubnet1/DefaultRoute": [ 134 | { 135 | "type": "aws:cdk:logicalId", 136 | "data": "TestVpcPrivateSubnet1DefaultRoute32E7B814" 137 | } 138 | ], 139 | "/integ-addCfnInitProxy-stack/TestVpc/PrivateSubnet2/Subnet": [ 140 | { 141 | "type": "aws:cdk:logicalId", 142 | "data": "TestVpcPrivateSubnet2SubnetDE0C64A2" 143 | } 144 | ], 145 | "/integ-addCfnInitProxy-stack/TestVpc/PrivateSubnet2/RouteTable": [ 146 | { 147 | "type": "aws:cdk:logicalId", 148 | "data": "TestVpcPrivateSubnet2RouteTableCEF29F7C" 149 | } 150 | ], 151 | "/integ-addCfnInitProxy-stack/TestVpc/PrivateSubnet2/RouteTableAssociation": [ 152 | { 153 | "type": "aws:cdk:logicalId", 154 | "data": "TestVpcPrivateSubnet2RouteTableAssociation18250AB4" 155 | } 156 | ], 157 | "/integ-addCfnInitProxy-stack/TestVpc/PrivateSubnet2/DefaultRoute": [ 158 | { 159 | "type": "aws:cdk:logicalId", 160 | "data": "TestVpcPrivateSubnet2DefaultRouteA7EB6930" 161 | } 162 | ], 163 | "/integ-addCfnInitProxy-stack/TestVpc/IGW": [ 164 | { 165 | "type": "aws:cdk:logicalId", 166 | "data": "TestVpcIGW9DD53F70" 167 | } 168 | ], 169 | "/integ-addCfnInitProxy-stack/TestVpc/VPCGW": [ 170 | { 171 | "type": "aws:cdk:logicalId", 172 | "data": "TestVpcVPCGWF1827B84" 173 | } 174 | ], 175 | "/integ-addCfnInitProxy-stack/TestVpc/RestrictDefaultSecurityGroupCustomResource/Default": [ 176 | { 177 | "type": "aws:cdk:logicalId", 178 | "data": "TestVpcRestrictDefaultSecurityGroupCustomResourceB02B826A" 179 | } 180 | ], 181 | "/integ-addCfnInitProxy-stack/Custom::VpcRestrictDefaultSGCustomResourceProvider/Role": [ 182 | { 183 | "type": "aws:cdk:logicalId", 184 | "data": "CustomVpcRestrictDefaultSGCustomResourceProviderRole26592FE0" 185 | } 186 | ], 187 | "/integ-addCfnInitProxy-stack/Custom::VpcRestrictDefaultSGCustomResourceProvider/Handler": [ 188 | { 189 | "type": "aws:cdk:logicalId", 190 | "data": "CustomVpcRestrictDefaultSGCustomResourceProviderHandlerDC833E5E" 191 | } 192 | ], 193 | "/integ-addCfnInitProxy-stack/TestInstance/InstanceSecurityGroup/Resource": [ 194 | { 195 | "type": "aws:cdk:logicalId", 196 | "data": "TestInstanceInstanceSecurityGroupFC9BD332" 197 | } 198 | ], 199 | "/integ-addCfnInitProxy-stack/TestInstance/InstanceRole/Resource": [ 200 | { 201 | "type": "aws:cdk:logicalId", 202 | "data": "TestInstanceInstanceRole73B579CC" 203 | } 204 | ], 205 | "/integ-addCfnInitProxy-stack/TestInstance/InstanceRole/DefaultPolicy/Resource": [ 206 | { 207 | "type": "aws:cdk:logicalId", 208 | "data": "TestInstanceInstanceRoleDefaultPolicyB37E8251" 209 | } 210 | ], 211 | "/integ-addCfnInitProxy-stack/TestInstance/InstanceProfile": [ 212 | { 213 | "type": "aws:cdk:logicalId", 214 | "data": "TestInstanceInstanceProfileD0E25910" 215 | } 216 | ], 217 | "/integ-addCfnInitProxy-stack/TestInstance/Resource": [ 218 | { 219 | "type": "aws:cdk:logicalId", 220 | "data": "TestInstance44016A9E16ad79d8fcd58511" 221 | } 222 | ], 223 | "/integ-addCfnInitProxy-stack/SsmParameterValue:--aws--service--ami-amazon-linux-latest--amzn-ami-hvm-x86_64-gp2:C96584B6-F00A-464E-AD19-53AFF4B05118.Parameter": [ 224 | { 225 | "type": "aws:cdk:logicalId", 226 | "data": "SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter" 227 | } 228 | ], 229 | "/integ-addCfnInitProxy-stack/TestSecret/Resource": [ 230 | { 231 | "type": "aws:cdk:logicalId", 232 | "data": "TestSecret16AF87B1" 233 | } 234 | ], 235 | "/integ-addCfnInitProxy-stack/BootstrapVersion": [ 236 | { 237 | "type": "aws:cdk:logicalId", 238 | "data": "BootstrapVersion" 239 | } 240 | ], 241 | "/integ-addCfnInitProxy-stack/CheckBootstrapVersion": [ 242 | { 243 | "type": "aws:cdk:logicalId", 244 | "data": "CheckBootstrapVersion" 245 | } 246 | ] 247 | }, 248 | "displayName": "integ-addCfnInitProxy-stack" 249 | }, 250 | "IntegTestDefaultTestDeployAssertE3E7D2A4.assets": { 251 | "type": "cdk:asset-manifest", 252 | "properties": { 253 | "file": "IntegTestDefaultTestDeployAssertE3E7D2A4.assets.json", 254 | "requiresBootstrapStackVersion": 6, 255 | "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" 256 | } 257 | }, 258 | "IntegTestDefaultTestDeployAssertE3E7D2A4": { 259 | "type": "aws:cloudformation:stack", 260 | "environment": "aws://unknown-account/unknown-region", 261 | "properties": { 262 | "templateFile": "IntegTestDefaultTestDeployAssertE3E7D2A4.template.json", 263 | "terminationProtection": false, 264 | "validateOnSynth": false, 265 | "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", 266 | "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", 267 | "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", 268 | "requiresBootstrapStackVersion": 6, 269 | "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", 270 | "additionalDependencies": [ 271 | "IntegTestDefaultTestDeployAssertE3E7D2A4.assets" 272 | ], 273 | "lookupRole": { 274 | "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", 275 | "requiresBootstrapStackVersion": 8, 276 | "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" 277 | } 278 | }, 279 | "dependencies": [ 280 | "IntegTestDefaultTestDeployAssertE3E7D2A4.assets" 281 | ], 282 | "metadata": { 283 | "/IntegTest/DefaultTest/DeployAssert/BootstrapVersion": [ 284 | { 285 | "type": "aws:cdk:logicalId", 286 | "data": "BootstrapVersion" 287 | } 288 | ], 289 | "/IntegTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ 290 | { 291 | "type": "aws:cdk:logicalId", 292 | "data": "CheckBootstrapVersion" 293 | } 294 | ] 295 | }, 296 | "displayName": "IntegTest/DefaultTest/DeployAssert" 297 | }, 298 | "Tree": { 299 | "type": "cdk:tree", 300 | "properties": { 301 | "file": "tree.json" 302 | } 303 | } 304 | } 305 | } -------------------------------------------------------------------------------- /test/integ/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.dev.json", 3 | "include": [ 4 | "./**/integ.*.ts" 5 | ], 6 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 7 | } 8 | -------------------------------------------------------------------------------- /test/patches/addCfnInitProxy.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Aspects, CfnElement, SecretValue, Stack } from 'aws-cdk-lib'; 6 | import { Match, Template } from 'aws-cdk-lib/assertions'; 7 | import { AutoScalingGroup, Signals } from 'aws-cdk-lib/aws-autoscaling'; 8 | import { 9 | CloudFormationInit, 10 | InitPackage, 11 | Instance, 12 | InstanceClass, 13 | InstanceSize, 14 | InstanceType, 15 | MachineImage, 16 | Vpc, 17 | } from 'aws-cdk-lib/aws-ec2'; 18 | import { CfnSecret, Secret } from 'aws-cdk-lib/aws-secretsmanager'; 19 | import { IConstruct } from 'constructs'; 20 | import { AddCfnInitProxy, ProxyType } from '../../src/patches/addCfnInitProxy'; 21 | 22 | let stack: Stack; 23 | 24 | beforeEach(() => { 25 | stack = new Stack(); 26 | }); 27 | 28 | describe('Adding cfn-init proxy values to EC2 instance', () => { 29 | test('http proxy with no password needed', () => { 30 | const vpc = new Vpc(stack, 'TestVpc'); 31 | new Instance(stack, 'TestInstance', { 32 | machineImage: MachineImage.latestAmazonLinux(), 33 | instanceType: InstanceType.of(InstanceClass.MEMORY5, InstanceSize.LARGE), 34 | vpc, 35 | init: CloudFormationInit.fromElements(InitPackage.yum('python3')), 36 | }); 37 | Aspects.of(stack).add( 38 | new AddCfnInitProxy({ 39 | proxyHost: 'example.com', 40 | proxyPort: 8080, 41 | }) 42 | ); 43 | const template = Template.fromStack(stack); 44 | template.hasResourceProperties('AWS::EC2::Instance', { 45 | UserData: { 46 | 'Fn::Base64': { 47 | 'Fn::Join': [ 48 | '', 49 | Match.arrayWith([ 50 | ' --http-proxy http://', 51 | 'example.com:8080', 52 | ' --http-proxy http://', 53 | 'example.com:8080', 54 | ]), 55 | ], 56 | }, 57 | }, 58 | }); 59 | }); 60 | test('https proxy with no password needed', () => { 61 | const vpc = new Vpc(stack, 'TestVpc'); 62 | new Instance(stack, 'TestInstance', { 63 | machineImage: MachineImage.latestAmazonLinux(), 64 | instanceType: InstanceType.of(InstanceClass.MEMORY5, InstanceSize.LARGE), 65 | vpc, 66 | init: CloudFormationInit.fromElements(InitPackage.yum('python3')), 67 | }); 68 | Aspects.of(stack).add( 69 | new AddCfnInitProxy({ 70 | proxyHost: 'example.com', 71 | proxyPort: 8080, 72 | proxyType: ProxyType.HTTPS, 73 | }) 74 | ); 75 | const template = Template.fromStack(stack); 76 | template.hasResourceProperties('AWS::EC2::Instance', { 77 | UserData: { 78 | 'Fn::Base64': { 79 | 'Fn::Join': [ 80 | '', 81 | Match.arrayWith([ 82 | ' --https-proxy https://', 83 | 'example.com:8080', 84 | ' --https-proxy https://', 85 | 'example.com:8080', 86 | ]), 87 | ], 88 | }, 89 | }, 90 | }); 91 | }); 92 | test('Proxy with password', () => { 93 | const vpc = new Vpc(stack, 'TestVpc'); 94 | new Instance(stack, 'TestInstance', { 95 | machineImage: MachineImage.latestAmazonLinux(), 96 | instanceType: InstanceType.of(InstanceClass.MEMORY5, InstanceSize.LARGE), 97 | vpc, 98 | init: CloudFormationInit.fromElements(InitPackage.yum('python3')), 99 | }); 100 | const secret = new Secret(stack, 'TestSecret', { 101 | secretObjectValue: { 102 | user: SecretValue.unsafePlainText('someUser'), 103 | password: SecretValue.unsafePlainText('superSecret123'), 104 | }, 105 | }); 106 | Aspects.of(stack).add( 107 | new AddCfnInitProxy({ 108 | proxyHost: 'example.com', 109 | proxyPort: 8080, 110 | proxyCredentials: secret, 111 | }) 112 | ); 113 | const template = Template.fromStack(stack); 114 | const secretConstruct = secret.node.findAll().find((x: IConstruct) => { 115 | return x instanceof CfnSecret; 116 | }); 117 | template.hasResourceProperties('AWS::EC2::Instance', { 118 | UserData: { 119 | 'Fn::Base64': { 120 | 'Fn::Join': [ 121 | '', 122 | Match.arrayWith([ 123 | ' --http-proxy http://', 124 | { 125 | 'Fn::Join': [ 126 | '', 127 | Match.arrayEquals([ 128 | '{{resolve:secretsmanager:', 129 | { Ref: stack.getLogicalId(secretConstruct as CfnElement) }, 130 | ':SecretString:user::}}:{{resolve:secretsmanager:', 131 | { Ref: stack.getLogicalId(secretConstruct as CfnElement) }, 132 | ':SecretString:password::}}@', 133 | ]), 134 | ], 135 | }, 136 | ]), 137 | ], 138 | }, 139 | }, 140 | }); 141 | }); 142 | }); 143 | 144 | describe('Adding cfn-init proxy values to EC2 launch config', () => { 145 | test('http proxy with no password needed', () => { 146 | const vpc = new Vpc(stack, 'TestVpc'); 147 | new AutoScalingGroup(stack, 'TestAutoscalingGroup', { 148 | vpc, 149 | machineImage: MachineImage.latestAmazonLinux(), 150 | instanceType: InstanceType.of(InstanceClass.MEMORY5, InstanceSize.LARGE), 151 | init: CloudFormationInit.fromElements(InitPackage.yum('python3')), 152 | signals: Signals.waitForAll(), 153 | }); 154 | Aspects.of(stack).add( 155 | new AddCfnInitProxy({ 156 | proxyHost: 'example.com', 157 | proxyPort: 8080, 158 | }) 159 | ); 160 | const template = Template.fromStack(stack); 161 | template.hasResourceProperties('AWS::AutoScaling::LaunchConfiguration', { 162 | UserData: { 163 | 'Fn::Base64': { 164 | 'Fn::Join': [ 165 | '', 166 | Match.arrayWith([ 167 | ' --http-proxy http://', 168 | 'example.com:8080', 169 | ' --http-proxy http://', 170 | 'example.com:8080', 171 | ]), 172 | ], 173 | }, 174 | }, 175 | }); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /test/patches/addLambdaEnvironmentVariables.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Aspects, Stack } from 'aws-cdk-lib'; 6 | import { Match, Template } from 'aws-cdk-lib/assertions'; 7 | import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; 8 | import { AddLambdaEnvironmentVariables } from '../../src/patches/addLambdaEnvironmentVariables'; 9 | 10 | let stack: Stack; 11 | 12 | beforeEach(() => { 13 | stack = new Stack(); 14 | }); 15 | 16 | describe('Add environment variables to all Lambda functions', () => { 17 | test('adds environment variable to multiple lambdas', () => { 18 | new Function(stack, 'TestFunction1', { 19 | code: Code.fromInline('def handler(event, context)\n print(event)'), 20 | runtime: Runtime.PYTHON_3_11, 21 | handler: 'index.handler', 22 | }); 23 | 24 | new Function(stack, 'TestFunction2', { 25 | code: Code.fromInline( 26 | 'def handler(event, context)\n print("something")' 27 | ), 28 | runtime: Runtime.PYTHON_3_11, 29 | handler: 'index.handler', 30 | }); 31 | 32 | Aspects.of(stack).add( 33 | new AddLambdaEnvironmentVariables({ 34 | myNeatEnvVariable: 'value 1', 35 | }) 36 | ); 37 | 38 | const template = Template.fromStack(stack); 39 | 40 | template.resourcePropertiesCountIs( 41 | 'AWS::Lambda::Function', 42 | { 43 | Environment: { 44 | Variables: Match.objectLike({ myNeatEnvVariable: 'value 1' }), 45 | }, 46 | }, 47 | 2 48 | ); 49 | }); 50 | 51 | test("adds environment variable to multiple lambdas, and doesn't remove existing", () => { 52 | new Function(stack, 'TestFunction1', { 53 | code: Code.fromInline('def handler(event, context)\n print(event)'), 54 | runtime: Runtime.PYTHON_3_11, 55 | handler: 'index.handler', 56 | environment: { 57 | originalKey: 'originalVar', 58 | }, 59 | }); 60 | 61 | new Function(stack, 'TestFunction2', { 62 | code: Code.fromInline( 63 | 'def handler(event, context)\n print("something")' 64 | ), 65 | runtime: Runtime.PYTHON_3_11, 66 | handler: 'index.handler', 67 | }); 68 | 69 | Aspects.of(stack).add( 70 | new AddLambdaEnvironmentVariables({ 71 | myNeatEnvVariable: 'value 1', 72 | }) 73 | ); 74 | 75 | const template = Template.fromStack(stack); 76 | 77 | template.resourcePropertiesCountIs( 78 | 'AWS::Lambda::Function', 79 | { 80 | Environment: { 81 | Variables: Match.objectLike({ myNeatEnvVariable: 'value 1' }), 82 | }, 83 | }, 84 | 2 85 | ); 86 | template.resourcePropertiesCountIs( 87 | 'AWS::Lambda::Function', 88 | { 89 | Environment: { 90 | Variables: Match.objectLike({ originalKey: 'originalVar' }), 91 | }, 92 | }, 93 | 1 94 | ); 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /test/patches/convertInlinePoliciesToManaged.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { App, Aspects, Stack, aws_iam as iam } from 'aws-cdk-lib'; 6 | import { Template } from 'aws-cdk-lib/assertions'; 7 | import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; 8 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 9 | import { AddPermissionBoundary } from '../../src/patches/addPermissionsBoundary'; 10 | import { ConvertInlinePoliciesToManaged } from '../../src/patches/convertInlinePoliciesToManaged'; 11 | import { 12 | ResourceExtractor, 13 | ResourceExtractorShareMethod, 14 | } from '../../src/patches/resource-extractor/resourceExtractor'; 15 | 16 | let app: App; 17 | let stack: Stack; 18 | 19 | const appStackName = 'AppStack'; 20 | 21 | describe('Updating Resource Types', () => { 22 | const policyPrefix = 'POLICY_PREFIX_'; 23 | beforeEach(() => { 24 | app = new App(); 25 | stack = new Stack(app, appStackName); 26 | }); 27 | 28 | test('Only ManagedPolicy objects exist', () => { 29 | const func = new Function(stack, 'TestLambda', { 30 | code: Code.fromInline(`def handler(event, context)\n print(event)`), 31 | handler: 'index.handler', 32 | runtime: Runtime.PYTHON_3_11, 33 | }); 34 | const bucket = new Bucket(stack, 'TestBucket'); 35 | bucket.grantReadWrite(func); 36 | 37 | Aspects.of(app).add(new ConvertInlinePoliciesToManaged()); 38 | app.synth(); 39 | 40 | const appTemplate = Template.fromStack(stack); 41 | appTemplate.resourceCountIs('AWS::IAM::Policy', 0); 42 | appTemplate.resourceCountIs('AWS::IAM::ManagedPolicy', 1); 43 | }); 44 | test('Passes along any overrides to ManagedPolicy', () => { 45 | const policyName = 'some-policy'; 46 | const policy = new iam.Policy(stack, 'MyPolicy', { 47 | policyName, 48 | }); 49 | policy.addStatements( 50 | new iam.PolicyStatement({ 51 | actions: ['s3:*'], 52 | resources: ['*'], 53 | }) 54 | ); 55 | Aspects.of(stack).add( 56 | new AddPermissionBoundary({ 57 | permissionsBoundaryPolicyName: 'SOME_BOUNDARY', 58 | policyPrefix, 59 | }) 60 | ); 61 | Aspects.of(stack).add(new ConvertInlinePoliciesToManaged()); 62 | const template = Template.fromStack(stack); 63 | let polices = template.findResources('AWS::IAM::ManagedPolicy'); 64 | console.log(`polices: ${polices}`); 65 | const names: string[] = []; 66 | let i = 0; 67 | for (const templatePolicy of Object.keys(polices)) { 68 | const tmpPolicy = polices[templatePolicy].Properties 69 | .ManagedPolicyName as string; 70 | names[i] = tmpPolicy; 71 | i++; 72 | } 73 | expect(names.length).toBe(1); 74 | expect(names[0].startsWith(policyPrefix)).toBe(true); 75 | const uniqness_length = 8; 76 | expect(names[0].length).toBe( 77 | policyPrefix.length + policyName.length + uniqness_length 78 | ); 79 | }); 80 | 81 | test('Function dependencies on policy are maintained', () => { 82 | const fn = new Function(stack, 'TestLambda', { 83 | code: Code.fromInline(`def handler(event, context)\n print(event)`), 84 | handler: 'index.handler', 85 | runtime: Runtime.PYTHON_3_11, 86 | }); 87 | fn.addToRolePolicy( 88 | new iam.PolicyStatement({ 89 | actions: ['s3:*'], 90 | resources: ['*'], 91 | }) 92 | ); 93 | 94 | Aspects.of(app).add(new ConvertInlinePoliciesToManaged()); 95 | app.synth(); 96 | const template = Template.fromStack(stack); 97 | let functions = template.findResources('AWS::Lambda::Function'); 98 | let i = 0; 99 | for (const name of Object.keys(functions)) { 100 | const deps = functions[name].DependsOn; 101 | expect(deps.length).toBe(2); 102 | i++; 103 | } 104 | expect(i).toBeGreaterThan(0); 105 | }); 106 | 107 | test('ManagedPolicy works with resource extractor', () => { 108 | const extractedStack = new Stack(app, 'TestExtractedStack'); 109 | const resourceTypesToExtract = [ 110 | 'AWS::IAM::Role', 111 | 'AWS::IAM::Policy', 112 | 'AWS::IAM::ManagedPolicy', 113 | ]; 114 | const func = new Function(stack, 'TestLambda', { 115 | code: Code.fromInline(`def handler(event, context)\n print(event)`), 116 | handler: 'index.handler', 117 | runtime: Runtime.PYTHON_3_11, 118 | }); 119 | const bucket = new Bucket(stack, 'TestBucket'); 120 | bucket.grantReadWrite(func); 121 | Aspects.of(stack).add(new ConvertInlinePoliciesToManaged()); 122 | const synthedApp = app.synth(); 123 | 124 | Aspects.of(app).add( 125 | new ResourceExtractor({ 126 | extractDestinationStack: extractedStack, 127 | stackArtifacts: synthedApp.stacks, 128 | valueShareMethod: ResourceExtractorShareMethod.CFN_OUTPUT, 129 | resourceTypesToExtract, 130 | }) 131 | ); 132 | 133 | app.synth({ force: true }); 134 | 135 | const extractedTemplate = Template.fromStack(extractedStack); 136 | const appTemplate = Template.fromStack(stack); 137 | // Extracted stack has IAM resources 138 | extractedTemplate.resourceCountIs('AWS::IAM::Role', 1); 139 | extractedTemplate.resourceCountIs('AWS::IAM::ManagedPolicy', 1); 140 | // Non-IAM resources present in app stack 141 | appTemplate.resourceCountIs('AWS::S3::Bucket', 1); 142 | appTemplate.resourceCountIs('AWS::Lambda::Function', 1); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /test/patches/removePublicAccessBlockConfiguration.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Aspects, Stack } from 'aws-cdk-lib'; 6 | import { Match, Template } from 'aws-cdk-lib/assertions'; 7 | import { BlockPublicAccess, Bucket } from 'aws-cdk-lib/aws-s3'; 8 | import { RemovePublicAccessBlockConfiguration } from '../../src/patches/removePublicAccessBlockConfiguration'; 9 | 10 | let stack: Stack; 11 | 12 | beforeEach(() => { 13 | stack = new Stack(); 14 | }); 15 | 16 | describe('Removal of PublicAccessBlockConfiguration', () => { 17 | test('Removes PublicAccessBlockConfiguration from S3 bucket', () => { 18 | new Bucket(stack, 'MyBucket', { 19 | blockPublicAccess: BlockPublicAccess.BLOCK_ALL, 20 | }); 21 | 22 | Aspects.of(stack).add(new RemovePublicAccessBlockConfiguration()); 23 | 24 | const template = Template.fromStack(stack); 25 | template.hasResourceProperties('AWS::S3::Bucket', { 26 | PublicAccessBlockConfiguration: Match.absent(), 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/patches/removeTags.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Aspects, Stack } from 'aws-cdk-lib'; 6 | import { Match, Template } from 'aws-cdk-lib/assertions'; 7 | import { CfnBackupPlan } from 'aws-cdk-lib/aws-backup'; 8 | import { Vpc } from 'aws-cdk-lib/aws-ec2'; 9 | import { RemoveTags } from '../../src/patches/removeTags'; 10 | 11 | let stack: Stack; 12 | 13 | beforeEach(() => { 14 | stack = new Stack(); 15 | }); 16 | 17 | describe('Remove Tags from resources', () => { 18 | test("Can remove from 'Tags' resource", () => { 19 | new Vpc(stack, 'testVpc'); 20 | 21 | Aspects.of(stack).add( 22 | new RemoveTags({ 23 | cloudformationResource: 'AWS::EC2::EIP', 24 | }) 25 | ); 26 | 27 | const template = Template.fromStack(stack); 28 | template.hasResourceProperties('AWS::EC2::EIP', { 29 | Tags: Match.absent(), 30 | }); 31 | }); 32 | 33 | test("Remove tags with different name than 'Tags'", () => { 34 | new CfnBackupPlan(stack, 'TestBackupplan', { 35 | backupPlan: { 36 | backupPlanName: 'testBackupPlan', 37 | backupPlanRule: [ 38 | { 39 | ruleName: 'SomeRule', 40 | targetBackupVault: 'SomeBackupVault', 41 | }, 42 | ], 43 | }, 44 | backupPlanTags: { 45 | SomeKey: 'SomeValue', 46 | }, 47 | }); 48 | Aspects.of(stack).add( 49 | new RemoveTags({ 50 | cloudformationResource: 'AWS::Backup::BackupPlan', 51 | tagPropertyName: 'BackupPlanTags', 52 | }) 53 | ); 54 | 55 | const template = Template.fromStack(stack); 56 | template.hasResourceProperties('AWS::Backup::BackupPlan', { 57 | BackupPlanTags: Match.absent(), 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/patches/resource-extractor/cfnStore.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { App, Stack } from 'aws-cdk-lib'; 6 | import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; 7 | import { Bucket } from 'aws-cdk-lib/aws-s3'; 8 | import { CfnStore } from '../../../src/patches/resource-extractor/cfnStore'; 9 | import { ResourceExtractorShareMethod } from '../../../src/patches/resource-extractor/resourceExtractor'; 10 | 11 | let app: App; 12 | let stack: Stack; 13 | let extractedStack: Stack; 14 | 15 | const appStackName = 'AppStack'; 16 | const extractedStackName = 'ExtractedStack'; 17 | 18 | describe('CfnStore', () => { 19 | const env = { 20 | account: '111111111111', 21 | region: 'us-east-2', 22 | }; 23 | 24 | beforeEach(() => { 25 | app = new App(); 26 | stack = new Stack(app, appStackName, { env }); 27 | extractedStack = new Stack(app, extractedStackName, { env }); 28 | }); 29 | 30 | test('Saving stack outputs from API lookup', () => { 31 | const func = new Function(stack, 'TestLambda', { 32 | code: Code.fromInline(`def handler(event, context)\n print(event)`), 33 | handler: 'index.handler', 34 | runtime: Runtime.PYTHON_3_11, 35 | }); 36 | const bucket = new Bucket(stack, 'TestBucket'); 37 | bucket.grantReadWrite(func); 38 | const synthedApp = app.synth(); 39 | 40 | const describeStackMock = jest.spyOn( 41 | CfnStore.prototype as any, 42 | 'describeStack' 43 | ); 44 | const mockStack = { 45 | Outputs: [ 46 | { 47 | ExportName: 'foo', 48 | OutputValue: 'bar', 49 | }, 50 | ], 51 | }; 52 | describeStackMock.mockImplementation(() => mockStack); 53 | 54 | const cfn = new CfnStore({ 55 | extractedStackName: extractedStack.stackName, 56 | region: 'us-east-2', 57 | stackArtifacts: synthedApp.stacks, 58 | valueShareMethod: ResourceExtractorShareMethod.API_LOOKUP, 59 | }); 60 | expect(describeStackMock).toBeCalled(); 61 | expect(cfn.templates).toHaveProperty('AppStack.Resources'); 62 | expect(cfn.extractedStackExports).toEqual({ foo: 'bar' }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /test/patches/resource-extractor/flattener.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Flattener } from '../../../src/patches/resource-extractor/flattener'; 6 | 7 | let complexObject: Object; 8 | 9 | describe('Flattener', () => { 10 | beforeEach(() => { 11 | complexObject = { 12 | Foo: { 13 | Bar: 'Baz', 14 | }, 15 | }; 16 | }); 17 | 18 | test('Flatten an object', () => { 19 | const flattened = Flattener.flattenObject(complexObject); 20 | expect(flattened).toEqual({ 'Foo.Bar': 'Baz' }); 21 | }); 22 | 23 | test('Lookup flattened key', () => { 24 | const lookup = Flattener.getValueByPath(complexObject, 'Foo.Bar'); 25 | expect(lookup).toEqual('Baz'); 26 | }); 27 | 28 | test('Set from flattened key', () => { 29 | Flattener.setToValue(complexObject, 'Foo.Bar', 'newValue'); 30 | expect(complexObject).toEqual({ Foo: { Bar: 'newValue' } }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/patches/setApiGatewayEndpointConfiguration.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { Aspects, Stack } from 'aws-cdk-lib'; 6 | import { Template } from 'aws-cdk-lib/assertions'; 7 | import { EndpointType, RestApi } from 'aws-cdk-lib/aws-apigateway'; 8 | import { SetApiGatewayEndpointConfiguration } from '../../src/patches/setApiGatewayEndpointConfiguration'; 9 | 10 | let stack: Stack; 11 | 12 | beforeEach(() => { 13 | stack = new Stack(); 14 | }); 15 | 16 | describe('Set Api Gateway endpoint configuration', () => { 17 | test('Sets API Gateway endpoint to regional when default Edge is selected', () => { 18 | const api = new RestApi(stack, 'TestRestApi'); 19 | api.root.addMethod('ANY'); 20 | Aspects.of(stack).add( 21 | new SetApiGatewayEndpointConfiguration({ 22 | endpointType: EndpointType.REGIONAL, 23 | }) 24 | ); 25 | 26 | const template = Template.fromStack(stack); 27 | template.hasResourceProperties('AWS::ApiGateway::RestApi', { 28 | EndpointConfiguration: { 29 | Types: ['REGIONAL'], 30 | }, 31 | Parameters: { 32 | endpointConfigurationTypes: 'REGIONAL', 33 | }, 34 | }); 35 | }); 36 | 37 | test('Sets API Gateway endpoint to regional when endpoint type is specfically selected', () => { 38 | const api = new RestApi(stack, 'TestRestApi', { 39 | endpointConfiguration: { 40 | types: [EndpointType.PRIVATE], 41 | }, 42 | }); 43 | api.root.addMethod('ANY'); 44 | Aspects.of(stack).add(new SetApiGatewayEndpointConfiguration()); 45 | 46 | const template = Template.fromStack(stack); 47 | template.hasResourceProperties('AWS::ApiGateway::RestApi', { 48 | EndpointConfiguration: { 49 | Types: ['REGIONAL'], 50 | }, 51 | Parameters: { 52 | endpointConfigurationTypes: 'REGIONAL', 53 | }, 54 | }); 55 | }); 56 | 57 | test('Sets API Gateway endpoint to private when using default endpoint type', () => { 58 | const api = new RestApi(stack, 'TestRestApi'); 59 | api.root.addMethod('ANY'); 60 | Aspects.of(stack).add( 61 | new SetApiGatewayEndpointConfiguration({ 62 | endpointType: EndpointType.PRIVATE, 63 | }) 64 | ); 65 | 66 | const template = Template.fromStack(stack); 67 | template.hasResourceProperties('AWS::ApiGateway::RestApi', { 68 | EndpointConfiguration: { 69 | Types: ['PRIVATE'], 70 | }, 71 | Parameters: { 72 | endpointConfigurationTypes: 'PRIVATE', 73 | }, 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /test/utils/utils.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | SPDX-License-Identifier: Apache-2.0 4 | */ 5 | import { getResourceId } from '../../src/utils/utils'; 6 | 7 | describe('Testing utils', () => { 8 | test('getResourceId returns valid name', () => { 9 | const nodePath = 'Default/TestInlinePolicy/Resource'; 10 | expect(getResourceId(nodePath)).toBe('Default-TestInlinePolicy-Resource'); 11 | }); 12 | test('getResourceId returns valid name and replaces illegal characters for IAM role,policy and instance profile names', () => { 13 | const nodePath = 'Default/TestInlinePolicy/Resource:extra:stuff'; 14 | expect(getResourceId(nodePath)).toBe( 15 | 'Default-TestInlinePolicy-Resource-extra-stuff' 16 | ); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------