├── .eslintrc.json
├── .gitattributes
├── .github
├── dependabot.yml
├── pull_request_template.md
└── workflows
│ ├── build.yml
│ ├── pull-request-lint.yml
│ └── release.yml
├── .gitignore
├── .mergify.yml
├── .npmignore
├── .projen
├── deps.json
├── files.json
└── tasks.json
├── .projenrc.js
├── API.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── NOTICE
├── README.md
├── package.json
├── src
├── index.ts
└── utils.ts
├── test
├── inflater-deployment.yml
├── installation.test.ts
├── integ.karpenter.ts
├── roles.test.ts
├── serviceaccount.test.ts
├── utils.test.ts
└── versions.test.ts
├── tsconfig.dev.json
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 | {
3 | "env": {
4 | "jest": true,
5 | "node": true
6 | },
7 | "root": true,
8 | "plugins": [
9 | "@typescript-eslint",
10 | "import"
11 | ],
12 | "parser": "@typescript-eslint/parser",
13 | "parserOptions": {
14 | "ecmaVersion": 2018,
15 | "sourceType": "module",
16 | "project": "./tsconfig.dev.json"
17 | },
18 | "extends": [
19 | "plugin:import/typescript"
20 | ],
21 | "settings": {
22 | "import/parsers": {
23 | "@typescript-eslint/parser": [
24 | ".ts",
25 | ".tsx"
26 | ]
27 | },
28 | "import/resolver": {
29 | "node": {},
30 | "typescript": {
31 | "project": "./tsconfig.dev.json",
32 | "alwaysTryTypes": true
33 | }
34 | }
35 | },
36 | "ignorePatterns": [
37 | "*.js",
38 | "*.d.ts",
39 | "node_modules/",
40 | "*.generated.ts",
41 | "coverage",
42 | "!.projenrc.js"
43 | ],
44 | "rules": {
45 | "indent": [
46 | "off"
47 | ],
48 | "@typescript-eslint/indent": [
49 | "error",
50 | 2
51 | ],
52 | "quotes": [
53 | "error",
54 | "single",
55 | {
56 | "avoidEscape": true
57 | }
58 | ],
59 | "comma-dangle": [
60 | "error",
61 | "always-multiline"
62 | ],
63 | "comma-spacing": [
64 | "error",
65 | {
66 | "before": false,
67 | "after": true
68 | }
69 | ],
70 | "no-multi-spaces": [
71 | "error",
72 | {
73 | "ignoreEOLComments": false
74 | }
75 | ],
76 | "array-bracket-spacing": [
77 | "error",
78 | "never"
79 | ],
80 | "array-bracket-newline": [
81 | "error",
82 | "consistent"
83 | ],
84 | "object-curly-spacing": [
85 | "error",
86 | "always"
87 | ],
88 | "object-curly-newline": [
89 | "error",
90 | {
91 | "multiline": true,
92 | "consistent": true
93 | }
94 | ],
95 | "object-property-newline": [
96 | "error",
97 | {
98 | "allowAllPropertiesOnSameLine": true
99 | }
100 | ],
101 | "keyword-spacing": [
102 | "error"
103 | ],
104 | "brace-style": [
105 | "error",
106 | "1tbs",
107 | {
108 | "allowSingleLine": true
109 | }
110 | ],
111 | "space-before-blocks": [
112 | "error"
113 | ],
114 | "curly": [
115 | "error",
116 | "multi-line",
117 | "consistent"
118 | ],
119 | "@typescript-eslint/member-delimiter-style": [
120 | "error"
121 | ],
122 | "semi": [
123 | "error",
124 | "always"
125 | ],
126 | "max-len": [
127 | "error",
128 | {
129 | "code": 150,
130 | "ignoreUrls": true,
131 | "ignoreStrings": true,
132 | "ignoreTemplateLiterals": true,
133 | "ignoreComments": true,
134 | "ignoreRegExpLiterals": true
135 | }
136 | ],
137 | "quote-props": [
138 | "error",
139 | "consistent-as-needed"
140 | ],
141 | "@typescript-eslint/no-require-imports": [
142 | "error"
143 | ],
144 | "import/no-extraneous-dependencies": [
145 | "error",
146 | {
147 | "devDependencies": [
148 | "**/test/**",
149 | "**/build-tools/**"
150 | ],
151 | "optionalDependencies": false,
152 | "peerDependencies": true
153 | }
154 | ],
155 | "import/no-unresolved": [
156 | "error"
157 | ],
158 | "import/order": [
159 | "warn",
160 | {
161 | "groups": [
162 | "builtin",
163 | "external"
164 | ],
165 | "alphabetize": {
166 | "order": "asc",
167 | "caseInsensitive": true
168 | }
169 | }
170 | ],
171 | "no-duplicate-imports": [
172 | "error"
173 | ],
174 | "no-shadow": [
175 | "off"
176 | ],
177 | "@typescript-eslint/no-shadow": [
178 | "error"
179 | ],
180 | "key-spacing": [
181 | "error"
182 | ],
183 | "no-multiple-empty-lines": [
184 | "error"
185 | ],
186 | "@typescript-eslint/no-floating-promises": [
187 | "error"
188 | ],
189 | "no-return-await": [
190 | "off"
191 | ],
192 | "@typescript-eslint/return-await": [
193 | "error"
194 | ],
195 | "no-trailing-spaces": [
196 | "error"
197 | ],
198 | "dot-notation": [
199 | "error"
200 | ],
201 | "no-bitwise": [
202 | "error"
203 | ],
204 | "@typescript-eslint/member-ordering": [
205 | "error",
206 | {
207 | "default": [
208 | "public-static-field",
209 | "public-static-method",
210 | "protected-static-field",
211 | "protected-static-method",
212 | "private-static-field",
213 | "private-static-method",
214 | "field",
215 | "constructor",
216 | "method"
217 | ]
218 | }
219 | ]
220 | },
221 | "overrides": [
222 | {
223 | "files": [
224 | ".projenrc.js"
225 | ],
226 | "rules": {
227 | "@typescript-eslint/no-require-imports": "off",
228 | "import/no-extraneous-dependencies": "off"
229 | }
230 | }
231 | ]
232 | }
233 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 |
3 | *.snap linguist-generated
4 | /.eslintrc.json linguist-generated
5 | /.gitattributes linguist-generated
6 | /.github/dependabot.yml linguist-generated
7 | /.github/pull_request_template.md linguist-generated
8 | /.github/workflows/build.yml linguist-generated
9 | /.github/workflows/pull-request-lint.yml linguist-generated
10 | /.github/workflows/release.yml linguist-generated
11 | /.gitignore linguist-generated
12 | /.mergify.yml linguist-generated
13 | /.npmignore linguist-generated
14 | /.projen/** linguist-generated
15 | /.projen/deps.json linguist-generated
16 | /.projen/files.json linguist-generated
17 | /.projen/tasks.json linguist-generated
18 | /API.md linguist-generated
19 | /LICENSE linguist-generated
20 | /package.json linguist-generated
21 | /tsconfig.dev.json linguist-generated
22 | /yarn.lock linguist-generated
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 |
3 | version: 2
4 | updates:
5 | - package-ecosystem: npm
6 | versioning-strategy: lockfile-only
7 | directory: /
8 | schedule:
9 | interval: monthly
10 | ignore:
11 | - dependency-name: projen
12 | target-branch: develop
13 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ---
2 | *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 |
3 | name: build
4 | on:
5 | pull_request: {}
6 | workflow_dispatch: {}
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: write
12 | outputs:
13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }}
14 | env:
15 | CI: "true"
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v3
19 | with:
20 | ref: ${{ github.event.pull_request.head.ref }}
21 | repository: ${{ github.event.pull_request.head.repo.full_name }}
22 | - name: Setup Node.js
23 | uses: actions/setup-node@v3
24 | with:
25 | node-version: 18.x
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 | - name: Upload patch
36 | if: steps.self_mutation.outputs.self_mutation_happened
37 | uses: actions/upload-artifact@v4.4.3
38 | with:
39 | name: .repo.patch
40 | path: .repo.patch
41 | include-hidden-files: true
42 | - name: Fail build on mutation
43 | if: steps.self_mutation.outputs.self_mutation_happened
44 | run: |-
45 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch."
46 | cat .repo.patch
47 | exit 1
48 | - name: Backup artifact permissions
49 | run: cd dist && getfacl -R . > permissions-backup.acl
50 | continue-on-error: true
51 | - name: Upload artifact
52 | uses: actions/upload-artifact@v4.4.3
53 | with:
54 | name: build-artifact
55 | path: dist
56 | include-hidden-files: true
57 | self-mutation:
58 | needs: build
59 | runs-on: ubuntu-latest
60 | permissions:
61 | contents: write
62 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository)
63 | steps:
64 | - name: Checkout
65 | uses: actions/checkout@v3
66 | with:
67 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }}
68 | ref: ${{ github.event.pull_request.head.ref }}
69 | repository: ${{ github.event.pull_request.head.repo.full_name }}
70 | - name: Download patch
71 | uses: actions/download-artifact@v4.1.8
72 | with:
73 | name: .repo.patch
74 | path: ${{ runner.temp }}
75 | - name: Apply patch
76 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."'
77 | - name: Set git identity
78 | run: |-
79 | git config user.name "github-actions"
80 | git config user.email "github-actions@github.com"
81 | - name: Push changes
82 | env:
83 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }}
84 | run: |-
85 | git add .
86 | git commit -s -m "chore: self mutation"
87 | git push origin HEAD:$PULL_REQUEST_REF
88 | package-js:
89 | needs: build
90 | runs-on: ubuntu-latest
91 | permissions: {}
92 | if: "! needs.build.outputs.self_mutation_happened"
93 | steps:
94 | - uses: actions/setup-node@v3
95 | with:
96 | node-version: 18.x
97 | - name: Download build artifacts
98 | uses: actions/download-artifact@v4.1.8
99 | with:
100 | name: build-artifact
101 | path: dist
102 | - name: Restore build artifact permissions
103 | run: cd dist && setfacl --restore=permissions-backup.acl
104 | continue-on-error: true
105 | - name: Prepare Repository
106 | run: mv dist .repo
107 | - name: Install Dependencies
108 | run: cd .repo && yarn install --check-files --frozen-lockfile
109 | - name: Create js artifact
110 | run: cd .repo && npx projen package:js
111 | - name: Collect js Artifact
112 | run: mv .repo/dist dist
113 | package-python:
114 | needs: build
115 | runs-on: ubuntu-latest
116 | permissions: {}
117 | if: "! needs.build.outputs.self_mutation_happened"
118 | steps:
119 | - uses: actions/setup-node@v3
120 | with:
121 | node-version: 18.x
122 | - uses: actions/setup-python@v4
123 | with:
124 | python-version: 3.x
125 | - name: Download build artifacts
126 | uses: actions/download-artifact@v4.1.8
127 | with:
128 | name: build-artifact
129 | path: dist
130 | - name: Restore build artifact permissions
131 | run: cd dist && setfacl --restore=permissions-backup.acl
132 | continue-on-error: true
133 | - name: Prepare Repository
134 | run: mv dist .repo
135 | - name: Install Dependencies
136 | run: cd .repo && yarn install --check-files --frozen-lockfile
137 | - name: Create python artifact
138 | run: cd .repo && npx projen package:python
139 | - name: Collect python Artifact
140 | run: mv .repo/dist dist
141 |
--------------------------------------------------------------------------------
/.github/workflows/pull-request-lint.yml:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 |
3 | name: pull-request-lint
4 | on:
5 | pull_request_target:
6 | types:
7 | - labeled
8 | - opened
9 | - synchronize
10 | - reopened
11 | - ready_for_review
12 | - edited
13 | jobs:
14 | validate:
15 | name: Validate PR title
16 | runs-on: ubuntu-latest
17 | permissions:
18 | pull-requests: write
19 | steps:
20 | - uses: amannn/action-semantic-pull-request@v5.0.2
21 | env:
22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 | with:
24 | types: |-
25 | feat
26 | fix
27 | chore
28 | requireScope: false
29 | githubBaseUrl: ${{ github.api_url }}
30 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 |
3 | name: release
4 | on:
5 | push:
6 | branches:
7 | - main
8 | workflow_dispatch: {}
9 | jobs:
10 | release:
11 | runs-on: ubuntu-latest
12 | permissions:
13 | contents: write
14 | outputs:
15 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }}
16 | env:
17 | CI: "true"
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v3
21 | with:
22 | fetch-depth: 0
23 | - name: Set git identity
24 | run: |-
25 | git config user.name "github-actions"
26 | git config user.email "github-actions@github.com"
27 | - name: Setup Node.js
28 | uses: actions/setup-node@v3
29 | with:
30 | node-version: 18.x
31 | - name: Install dependencies
32 | run: yarn install --check-files --frozen-lockfile
33 | - name: release
34 | run: npx projen release
35 | - name: Check for new commits
36 | id: git_remote
37 | run: echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT
38 | - name: Backup artifact permissions
39 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }}
40 | run: cd dist && getfacl -R . > permissions-backup.acl
41 | continue-on-error: true
42 | - name: Upload artifact
43 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }}
44 | uses: actions/upload-artifact@v4.4.3
45 | with:
46 | name: build-artifact
47 | path: dist
48 | include-hidden-files: true
49 | release_github:
50 | name: Publish to GitHub Releases
51 | needs: release
52 | runs-on: ubuntu-latest
53 | permissions:
54 | contents: write
55 | if: needs.release.outputs.latest_commit == github.sha
56 | steps:
57 | - uses: actions/setup-node@v3
58 | with:
59 | node-version: 18.x
60 | - name: Download build artifacts
61 | uses: actions/download-artifact@v4.1.8
62 | with:
63 | name: build-artifact
64 | path: dist
65 | - name: Restore build artifact permissions
66 | run: cd dist && setfacl --restore=permissions-backup.acl
67 | continue-on-error: true
68 | - name: Prepare Repository
69 | run: mv dist .repo
70 | - name: Collect GitHub Metadata
71 | run: mv .repo/dist dist
72 | - name: Release
73 | env:
74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75 | GITHUB_REPOSITORY: ${{ github.repository }}
76 | GITHUB_REF: ${{ github.ref }}
77 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi
78 | release_npm:
79 | name: Publish to npm
80 | needs: release
81 | runs-on: ubuntu-latest
82 | permissions:
83 | contents: read
84 | if: needs.release.outputs.latest_commit == github.sha
85 | steps:
86 | - uses: actions/setup-node@v3
87 | with:
88 | node-version: 18.x
89 | - name: Download build artifacts
90 | uses: actions/download-artifact@v4.1.8
91 | with:
92 | name: build-artifact
93 | path: dist
94 | - name: Restore build artifact permissions
95 | run: cd dist && setfacl --restore=permissions-backup.acl
96 | continue-on-error: true
97 | - name: Prepare Repository
98 | run: mv dist .repo
99 | - name: Install Dependencies
100 | run: cd .repo && yarn install --check-files --frozen-lockfile
101 | - name: Create js artifact
102 | run: cd .repo && npx projen package:js
103 | - name: Collect js Artifact
104 | run: mv .repo/dist dist
105 | - name: Release
106 | env:
107 | NPM_DIST_TAG: latest
108 | NPM_REGISTRY: registry.npmjs.org
109 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
110 | run: npx -p publib@latest publib-npm
111 | release_pypi:
112 | name: Publish to PyPI
113 | needs: release
114 | runs-on: ubuntu-latest
115 | permissions:
116 | contents: read
117 | if: needs.release.outputs.latest_commit == github.sha
118 | steps:
119 | - uses: actions/setup-node@v3
120 | with:
121 | node-version: 18.x
122 | - uses: actions/setup-python@v4
123 | with:
124 | python-version: 3.x
125 | - name: Download build artifacts
126 | uses: actions/download-artifact@v4.1.8
127 | with:
128 | name: build-artifact
129 | path: dist
130 | - name: Restore build artifact permissions
131 | run: cd dist && setfacl --restore=permissions-backup.acl
132 | continue-on-error: true
133 | - name: Prepare Repository
134 | run: mv dist .repo
135 | - name: Install Dependencies
136 | run: cd .repo && yarn install --check-files --frozen-lockfile
137 | - name: Create python artifact
138 | run: cd .repo && npx projen package:python
139 | - name: Collect python Artifact
140 | run: mv .repo/dist dist
141 | - name: Release
142 | env:
143 | TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }}
144 | TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }}
145 | run: npx -p publib@latest publib-pypi
146 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 | !/.gitattributes
3 | !/.projen/tasks.json
4 | !/.projen/deps.json
5 | !/.projen/files.json
6 | !/.github/workflows/pull-request-lint.yml
7 | !/package.json
8 | !/LICENSE
9 | !/.npmignore
10 | logs
11 | *.log
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 | lerna-debug.log*
16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
17 | pids
18 | *.pid
19 | *.seed
20 | *.pid.lock
21 | lib-cov
22 | coverage
23 | *.lcov
24 | .nyc_output
25 | build/Release
26 | node_modules/
27 | jspm_packages/
28 | *.tsbuildinfo
29 | .eslintcache
30 | *.tgz
31 | .yarn-integrity
32 | .cache
33 | !/.projenrc.js
34 | /test-reports/
35 | junit.xml
36 | /coverage/
37 | !/.github/workflows/build.yml
38 | /dist/changelog.md
39 | /dist/version.txt
40 | !/.github/workflows/release.yml
41 | !/.mergify.yml
42 | !/.github/dependabot.yml
43 | !/.github/pull_request_template.md
44 | !/test/
45 | !/tsconfig.dev.json
46 | !/src/
47 | /lib
48 | /dist/
49 | !/.eslintrc.json
50 | .jsii
51 | tsconfig.json
52 | !/API.md
53 | cdk.out/
54 | cdk.context.json
55 | .env
56 |
--------------------------------------------------------------------------------
/.mergify.yml:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js 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-python
12 | pull_request_rules:
13 | - name: Automatic merge on approval and successful build
14 | actions:
15 | delete_head_branch: {}
16 | queue:
17 | method: squash
18 | name: default
19 | commit_message_template: |-
20 | {{ title }} (#{{ number }})
21 |
22 | {{ body }}
23 | conditions:
24 | - "#approved-reviews-by>=1"
25 | - -label~=(do-not-merge)
26 | - status-success=build
27 | - status-success=package-js
28 | - status-success=package-python
29 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 | /.projen/
3 | /test-reports/
4 | junit.xml
5 | /coverage/
6 | 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 | cdk.out/
26 | cdk.context.json
27 | .env
28 |
--------------------------------------------------------------------------------
/.projen/deps.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": [
3 | {
4 | "name": "@aws-cdk/lambda-layer-kubectl-v24",
5 | "type": "build"
6 | },
7 | {
8 | "name": "@aws-cdk/lambda-layer-kubectl-v25",
9 | "type": "build"
10 | },
11 | {
12 | "name": "@aws-cdk/lambda-layer-kubectl-v26",
13 | "type": "build"
14 | },
15 | {
16 | "name": "@aws-cdk/lambda-layer-kubectl-v27",
17 | "type": "build"
18 | },
19 | {
20 | "name": "@aws-cdk/lambda-layer-kubectl-v28",
21 | "type": "build"
22 | },
23 | {
24 | "name": "@aws-cdk/lambda-layer-kubectl-v29",
25 | "type": "build"
26 | },
27 | {
28 | "name": "@aws-cdk/lambda-layer-kubectl-v30",
29 | "type": "build"
30 | },
31 | {
32 | "name": "@aws-cdk/lambda-layer-kubectl-v31",
33 | "type": "build"
34 | },
35 | {
36 | "name": "@types/jest",
37 | "version": "^27",
38 | "type": "build"
39 | },
40 | {
41 | "name": "@types/node",
42 | "version": "^16",
43 | "type": "build"
44 | },
45 | {
46 | "name": "@typescript-eslint/eslint-plugin",
47 | "version": "^6",
48 | "type": "build"
49 | },
50 | {
51 | "name": "@typescript-eslint/parser",
52 | "version": "^6",
53 | "type": "build"
54 | },
55 | {
56 | "name": "eslint-import-resolver-node",
57 | "type": "build"
58 | },
59 | {
60 | "name": "eslint-import-resolver-typescript",
61 | "type": "build"
62 | },
63 | {
64 | "name": "eslint-plugin-import",
65 | "type": "build"
66 | },
67 | {
68 | "name": "eslint",
69 | "version": "^8",
70 | "type": "build"
71 | },
72 | {
73 | "name": "jest-junit",
74 | "version": "^15",
75 | "type": "build"
76 | },
77 | {
78 | "name": "jest",
79 | "version": "^27",
80 | "type": "build"
81 | },
82 | {
83 | "name": "jsii-diff",
84 | "type": "build"
85 | },
86 | {
87 | "name": "jsii-docgen",
88 | "type": "build"
89 | },
90 | {
91 | "name": "jsii-pacmak",
92 | "type": "build"
93 | },
94 | {
95 | "name": "jsii-rosetta",
96 | "version": "1.x",
97 | "type": "build"
98 | },
99 | {
100 | "name": "jsii",
101 | "version": "1.x",
102 | "type": "build"
103 | },
104 | {
105 | "name": "projen",
106 | "type": "build"
107 | },
108 | {
109 | "name": "standard-version",
110 | "version": "^9",
111 | "type": "build"
112 | },
113 | {
114 | "name": "ts-jest",
115 | "version": "^27",
116 | "type": "build"
117 | },
118 | {
119 | "name": "typescript",
120 | "type": "build"
121 | },
122 | {
123 | "name": "semver",
124 | "type": "bundled"
125 | },
126 | {
127 | "name": "@types/babel__traverse",
128 | "version": "7.18.2",
129 | "type": "override"
130 | },
131 | {
132 | "name": "@types/prettier",
133 | "version": "2.6.0",
134 | "type": "override"
135 | },
136 | {
137 | "name": "aws-cdk-lib",
138 | "version": "^2.178.2",
139 | "type": "peer"
140 | },
141 | {
142 | "name": "constructs",
143 | "version": "^10.0.5",
144 | "type": "peer"
145 | }
146 | ],
147 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"."
148 | }
149 |
--------------------------------------------------------------------------------
/.projen/files.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | ".eslintrc.json",
4 | ".gitattributes",
5 | ".github/dependabot.yml",
6 | ".github/pull_request_template.md",
7 | ".github/workflows/build.yml",
8 | ".github/workflows/pull-request-lint.yml",
9 | ".github/workflows/release.yml",
10 | ".gitignore",
11 | ".mergify.yml",
12 | ".projen/deps.json",
13 | ".projen/files.json",
14 | ".projen/tasks.json",
15 | "LICENSE",
16 | "tsconfig.dev.json"
17 | ],
18 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"."
19 | }
20 |
--------------------------------------------------------------------------------
/.projen/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": {
3 | "build": {
4 | "name": "build",
5 | "description": "Full release build",
6 | "steps": [
7 | {
8 | "spawn": "default"
9 | },
10 | {
11 | "spawn": "pre-compile"
12 | },
13 | {
14 | "spawn": "compile"
15 | },
16 | {
17 | "spawn": "post-compile"
18 | },
19 | {
20 | "spawn": "test"
21 | },
22 | {
23 | "spawn": "package"
24 | }
25 | ]
26 | },
27 | "bump": {
28 | "name": "bump",
29 | "description": "Bumps version based on latest git tag and generates a changelog entry",
30 | "env": {
31 | "OUTFILE": "package.json",
32 | "CHANGELOG": "dist/changelog.md",
33 | "BUMPFILE": "dist/version.txt",
34 | "RELEASETAG": "dist/releasetag.txt",
35 | "RELEASE_TAG_PREFIX": ""
36 | },
37 | "steps": [
38 | {
39 | "builtin": "release/bump-version"
40 | }
41 | ],
42 | "condition": "! git log --oneline -1 | grep -q \"chore(release):\""
43 | },
44 | "clobber": {
45 | "name": "clobber",
46 | "description": "hard resets to HEAD of origin and cleans the local repo",
47 | "env": {
48 | "BRANCH": "$(git branch --show-current)"
49 | },
50 | "steps": [
51 | {
52 | "exec": "git checkout -b scratch",
53 | "name": "save current HEAD in \"scratch\" branch"
54 | },
55 | {
56 | "exec": "git checkout $BRANCH"
57 | },
58 | {
59 | "exec": "git fetch origin",
60 | "name": "fetch latest changes from origin"
61 | },
62 | {
63 | "exec": "git reset --hard origin/$BRANCH",
64 | "name": "hard reset to origin commit"
65 | },
66 | {
67 | "exec": "git clean -fdx",
68 | "name": "clean all untracked files"
69 | },
70 | {
71 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)"
72 | }
73 | ],
74 | "condition": "git diff --exit-code > /dev/null"
75 | },
76 | "compat": {
77 | "name": "compat",
78 | "description": "Perform API compatibility check against latest version",
79 | "steps": [
80 | {
81 | "exec": "jsii-diff npm:$(node -p \"require('./package.json').name\") -k --ignore-file .compatignore || (echo \"\nUNEXPECTED BREAKING CHANGES: add keys such as 'removed:constructs.Node.of' to .compatignore to skip.\n\" && exit 1)"
82 | }
83 | ]
84 | },
85 | "compile": {
86 | "name": "compile",
87 | "description": "Only compile",
88 | "steps": [
89 | {
90 | "exec": "jsii --silence-warnings=reserved-word"
91 | }
92 | ]
93 | },
94 | "default": {
95 | "name": "default",
96 | "description": "Synthesize project files",
97 | "steps": [
98 | {
99 | "exec": "node .projenrc.js"
100 | }
101 | ]
102 | },
103 | "docgen": {
104 | "name": "docgen",
105 | "description": "Generate API.md from .jsii manifest",
106 | "steps": [
107 | {
108 | "exec": "jsii-docgen -o API.md"
109 | }
110 | ]
111 | },
112 | "eject": {
113 | "name": "eject",
114 | "description": "Remove projen from the project",
115 | "env": {
116 | "PROJEN_EJECTING": "true"
117 | },
118 | "steps": [
119 | {
120 | "spawn": "default"
121 | }
122 | ]
123 | },
124 | "eslint": {
125 | "name": "eslint",
126 | "description": "Runs eslint against the codebase",
127 | "steps": [
128 | {
129 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test build-tools .projenrc.js"
130 | }
131 | ]
132 | },
133 | "install": {
134 | "name": "install",
135 | "description": "Install project dependencies and update lockfile (non-frozen)",
136 | "steps": [
137 | {
138 | "exec": "yarn install --check-files"
139 | }
140 | ]
141 | },
142 | "install:ci": {
143 | "name": "install:ci",
144 | "description": "Install project dependencies using frozen lockfile",
145 | "steps": [
146 | {
147 | "exec": "yarn install --check-files --frozen-lockfile"
148 | }
149 | ]
150 | },
151 | "package": {
152 | "name": "package",
153 | "description": "Creates the distribution package",
154 | "steps": [
155 | {
156 | "exec": "if [ ! -z ${CI} ]; then rsync -a . .repo --exclude .git --exclude node_modules && rm -rf dist && mv .repo dist; else npx projen package-all; fi"
157 | }
158 | ]
159 | },
160 | "package-all": {
161 | "name": "package-all",
162 | "description": "Packages artifacts for all target languages",
163 | "steps": [
164 | {
165 | "spawn": "package:js"
166 | },
167 | {
168 | "spawn": "package:python"
169 | }
170 | ]
171 | },
172 | "package:js": {
173 | "name": "package:js",
174 | "description": "Create js language bindings",
175 | "steps": [
176 | {
177 | "exec": "jsii-pacmak -v --target js"
178 | }
179 | ]
180 | },
181 | "package:python": {
182 | "name": "package:python",
183 | "description": "Create python language bindings",
184 | "steps": [
185 | {
186 | "exec": "jsii-pacmak -v --target python"
187 | }
188 | ]
189 | },
190 | "post-compile": {
191 | "name": "post-compile",
192 | "description": "Runs after successful compilation",
193 | "steps": [
194 | {
195 | "spawn": "docgen"
196 | }
197 | ]
198 | },
199 | "pre-compile": {
200 | "name": "pre-compile",
201 | "description": "Prepare the project for compilation"
202 | },
203 | "release": {
204 | "name": "release",
205 | "description": "Prepare a release from \"main\" branch",
206 | "env": {
207 | "RELEASE": "true",
208 | "MAJOR": "1"
209 | },
210 | "steps": [
211 | {
212 | "exec": "rm -fr dist"
213 | },
214 | {
215 | "spawn": "bump"
216 | },
217 | {
218 | "spawn": "build"
219 | },
220 | {
221 | "spawn": "unbump"
222 | },
223 | {
224 | "exec": "git diff --ignore-space-at-eol --exit-code"
225 | }
226 | ]
227 | },
228 | "test": {
229 | "name": "test",
230 | "description": "Run tests",
231 | "steps": [
232 | {
233 | "exec": "jest --passWithNoTests --updateSnapshot",
234 | "receiveArgs": true
235 | },
236 | {
237 | "spawn": "eslint"
238 | }
239 | ]
240 | },
241 | "test:deploy": {
242 | "name": "test:deploy",
243 | "steps": [
244 | {
245 | "exec": "npx cdk deploy -a \"npx ts-node -P tsconfig.dev.json --prefer-ts-exts test/integ.karpenter.ts\""
246 | }
247 | ]
248 | },
249 | "test:destroy": {
250 | "name": "test:destroy",
251 | "steps": [
252 | {
253 | "exec": "npx cdk destroy -a \"npx ts-node -P tsconfig.dev.json --prefer-ts-exts test/integ.karpenter.ts\""
254 | }
255 | ]
256 | },
257 | "test:synth": {
258 | "name": "test:synth",
259 | "steps": [
260 | {
261 | "exec": "npx cdk synth -a \"npx ts-node -P tsconfig.dev.json --prefer-ts-exts test/integ.karpenter.ts\""
262 | }
263 | ]
264 | },
265 | "test:watch": {
266 | "name": "test:watch",
267 | "description": "Run jest in watch mode",
268 | "steps": [
269 | {
270 | "exec": "jest --watch"
271 | }
272 | ]
273 | },
274 | "unbump": {
275 | "name": "unbump",
276 | "description": "Restores version to 0.0.0",
277 | "env": {
278 | "OUTFILE": "package.json",
279 | "CHANGELOG": "dist/changelog.md",
280 | "BUMPFILE": "dist/version.txt",
281 | "RELEASETAG": "dist/releasetag.txt",
282 | "RELEASE_TAG_PREFIX": ""
283 | },
284 | "steps": [
285 | {
286 | "builtin": "release/reset-version"
287 | }
288 | ]
289 | },
290 | "watch": {
291 | "name": "watch",
292 | "description": "Watch & compile in the background",
293 | "steps": [
294 | {
295 | "exec": "jsii -w --silence-warnings=reserved-word"
296 | }
297 | ]
298 | }
299 | },
300 | "env": {
301 | "PATH": "$(npx -c \"node --print process.env.PATH\")"
302 | },
303 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"."
304 | }
305 |
--------------------------------------------------------------------------------
/.projenrc.js:
--------------------------------------------------------------------------------
1 | const { awscdk, JsonPatch } = require('projen');
2 | const { DependabotScheduleInterval } = require('projen/lib/github');
3 |
4 | const PROJECT_NAME = 'cdk-eks-karpenter';
5 |
6 | const project = new awscdk.AwsCdkConstructLibrary({
7 | author: 'Andreas Lindh',
8 | authorAddress: 'elindh@amazon.com',
9 | description: 'CDK construct library that allows you install Karpenter in an AWS EKS cluster',
10 | keywords: ['eks', 'karpenter'],
11 | cdkVersion: '2.178.2',
12 |
13 | majorVersion: 1,
14 |
15 | devDeps: [
16 | '@aws-cdk/lambda-layer-kubectl-v24',
17 | '@aws-cdk/lambda-layer-kubectl-v25',
18 | '@aws-cdk/lambda-layer-kubectl-v26',
19 | '@aws-cdk/lambda-layer-kubectl-v27',
20 | '@aws-cdk/lambda-layer-kubectl-v28',
21 | '@aws-cdk/lambda-layer-kubectl-v29',
22 | '@aws-cdk/lambda-layer-kubectl-v30',
23 | '@aws-cdk/lambda-layer-kubectl-v31',
24 | ],
25 | bundledDeps: [
26 | 'semver',
27 | ],
28 | defaultReleaseBranch: 'main',
29 | name: PROJECT_NAME,
30 | repositoryUrl: 'https://github.com/aws-samples/cdk-eks-karpenter.git',
31 |
32 | pullRequestTemplateContents: [
33 | '---',
34 | '*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*',
35 | ],
36 |
37 | publishToPypi: {
38 | distName: PROJECT_NAME,
39 | module: 'cdk_eks_karpenter',
40 | },
41 |
42 | dependabot: true,
43 | dependabotOptions: {
44 | scheduleInterval: DependabotScheduleInterval.MONTHLY,
45 | },
46 | });
47 |
48 | // Patch dependabot configuration to file pull requests against develop branch instead
49 | dependabot_cfg = project.tryFindObjectFile('.github/dependabot.yml');
50 | dependabot_cfg.patch(JsonPatch.add('/updates/0/target-branch', 'develop'));
51 |
52 | const common_excludes = [
53 | 'cdk.out/',
54 | 'cdk.context.json',
55 | '.env',
56 | ];
57 | project.gitignore.exclude(...common_excludes);
58 | project.npmignore.exclude(...common_excludes);
59 |
60 | project.addTask('test:deploy', {
61 | exec: 'npx cdk deploy -a "npx ts-node -P tsconfig.dev.json --prefer-ts-exts test/integ.karpenter.ts"',
62 | });
63 | project.addTask('test:destroy', {
64 | exec: 'npx cdk destroy -a "npx ts-node -P tsconfig.dev.json --prefer-ts-exts test/integ.karpenter.ts"',
65 | });
66 | project.addTask('test:synth', {
67 | exec: 'npx cdk synth -a "npx ts-node -P tsconfig.dev.json --prefer-ts-exts test/integ.karpenter.ts"',
68 | });
69 |
70 | project.github.actions.set('actions/download-artifact', 'actions/download-artifact@v4.1.8');
71 | project.github.actions.set('actions/upload-artifact', 'actions/upload-artifact@v4.4.3');
72 |
73 | // https://github.com/actions/upload-artifact/issues/602
74 | build_workflow = project.tryFindObjectFile('.github/workflows/build.yml');
75 | build_workflow.patch(JsonPatch.add('/jobs/build/steps/5/with/include-hidden-files', true));
76 | build_workflow.patch(JsonPatch.add('/jobs/build/steps/8/with/include-hidden-files', true));
77 |
78 | release_workflow = project.tryFindObjectFile('.github/workflows/release.yml');
79 | release_workflow.patch(JsonPatch.add('/jobs/release/steps/7/with/include-hidden-files', true));
80 |
81 | project.synth();
82 |
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 | # API Reference
2 |
3 | ## Constructs
4 |
5 | ### Karpenter
6 |
7 | #### Initializers
8 |
9 | ```typescript
10 | import { Karpenter } from 'cdk-eks-karpenter'
11 |
12 | new Karpenter(scope: Construct, id: string, props: KarpenterProps)
13 | ```
14 |
15 | | **Name** | **Type** | **Description** |
16 | | --- | --- | --- |
17 | | scope
| constructs.Construct
| *No description.* |
18 | | id
| string
| *No description.* |
19 | | props
| KarpenterProps
| *No description.* |
20 |
21 | ---
22 |
23 | ##### `scope`Required
24 |
25 | - *Type:* constructs.Construct
26 |
27 | ---
28 |
29 | ##### `id`Required
30 |
31 | - *Type:* string
32 |
33 | ---
34 |
35 | ##### `props`Required
36 |
37 | - *Type:* KarpenterProps
38 |
39 | ---
40 |
41 | #### Methods
42 |
43 | | **Name** | **Description** |
44 | | --- | --- |
45 | | toString
| Returns a string representation of this construct. |
46 | | addEC2NodeClass
| addEC2NodeClass adds a EC2NodeClass to the Karpenter configuration. |
47 | | addManagedPolicyToKarpenterRole
| addManagedPolicyToKarpenterRole adds Managed Policies To Karpenter Role. |
48 | | addNodePool
| addNodePool adds a NodePool to the Karpenter configuration. |
49 | | addNodeTemplate
| addNodeTemplate adds a node template manifest to the cluster. |
50 | | addProvisioner
| addProvisioner adds a provisioner manifest to the cluster. |
51 |
52 | ---
53 |
54 | ##### `toString`
55 |
56 | ```typescript
57 | public toString(): string
58 | ```
59 |
60 | Returns a string representation of this construct.
61 |
62 | ##### `addEC2NodeClass`
63 |
64 | ```typescript
65 | public addEC2NodeClass(id: string, ec2NodeClassSpec: {[ key: string ]: any}): {[ key: string ]: any}
66 | ```
67 |
68 | addEC2NodeClass adds a EC2NodeClass to the Karpenter configuration.
69 |
70 | ###### `id`Required
71 |
72 | - *Type:* string
73 |
74 | must consist of lower case alphanumeric characters, \'-\' or \'.\', and must start and end with an alphanumeric character.
75 |
76 | ---
77 |
78 | ###### `ec2NodeClassSpec`Required
79 |
80 | - *Type:* {[ key: string ]: any}
81 |
82 | spec of Karpenters EC2NodeClass API.
83 |
84 | ---
85 |
86 | ##### `addManagedPolicyToKarpenterRole`
87 |
88 | ```typescript
89 | public addManagedPolicyToKarpenterRole(managedPolicy: IManagedPolicy): void
90 | ```
91 |
92 | addManagedPolicyToKarpenterRole adds Managed Policies To Karpenter Role.
93 |
94 | ###### `managedPolicy`Required
95 |
96 | - *Type:* aws-cdk-lib.aws_iam.IManagedPolicy
97 |
98 | iam managed policy to add to the karpenter role.
99 |
100 | ---
101 |
102 | ##### `addNodePool`
103 |
104 | ```typescript
105 | public addNodePool(id: string, nodePoolSpec: {[ key: string ]: any}): {[ key: string ]: any}
106 | ```
107 |
108 | addNodePool adds a NodePool to the Karpenter configuration.
109 |
110 | ###### `id`Required
111 |
112 | - *Type:* string
113 |
114 | must consist of lower case alphanumeric characters, \'-\' or \'.\', and must start and end with an alphanumeric character.
115 |
116 | ---
117 |
118 | ###### `nodePoolSpec`Required
119 |
120 | - *Type:* {[ key: string ]: any}
121 |
122 | spec of Karpenters NodePool API.
123 |
124 | ---
125 |
126 | ##### ~~`addNodeTemplate`~~
127 |
128 | ```typescript
129 | public addNodeTemplate(id: string, nodeTemplateSpec: {[ key: string ]: any}): void
130 | ```
131 |
132 | addNodeTemplate adds a node template manifest to the cluster.
133 |
134 | Currently the node template spec
135 | parameter is relatively free form.
136 |
137 | ###### `id`Required
138 |
139 | - *Type:* string
140 |
141 | must consist of lower case alphanumeric characters, \'-\' or \'.\', and must start and end with an alphanumeric character.
142 |
143 | ---
144 |
145 | ###### `nodeTemplateSpec`Required
146 |
147 | - *Type:* {[ key: string ]: any}
148 |
149 | spec of Karpenters Node Template object.
150 |
151 | ---
152 |
153 | ##### ~~`addProvisioner`~~
154 |
155 | ```typescript
156 | public addProvisioner(id: string, provisionerSpec: {[ key: string ]: any}): void
157 | ```
158 |
159 | addProvisioner adds a provisioner manifest to the cluster.
160 |
161 | Currently the provisioner spec
162 | parameter is relatively free form.
163 |
164 | ###### `id`Required
165 |
166 | - *Type:* string
167 |
168 | must consist of lower case alphanumeric characters, \'-\' or \'.\', and must start and end with an alphanumeric character.
169 |
170 | ---
171 |
172 | ###### `provisionerSpec`Required
173 |
174 | - *Type:* {[ key: string ]: any}
175 |
176 | spec of Karpenters Provisioner object.
177 |
178 | ---
179 |
180 | #### Static Functions
181 |
182 | | **Name** | **Description** |
183 | | --- | --- |
184 | | isConstruct
| Checks if `x` is a construct. |
185 |
186 | ---
187 |
188 | ##### ~~`isConstruct`~~
189 |
190 | ```typescript
191 | import { Karpenter } from 'cdk-eks-karpenter'
192 |
193 | Karpenter.isConstruct(x: any)
194 | ```
195 |
196 | Checks if `x` is a construct.
197 |
198 | ###### `x`Required
199 |
200 | - *Type:* any
201 |
202 | Any object.
203 |
204 | ---
205 |
206 | #### Properties
207 |
208 | | **Name** | **Type** | **Description** |
209 | | --- | --- | --- |
210 | | node
| constructs.Node
| The tree node. |
211 | | cluster
| aws-cdk-lib.aws_eks.Cluster
| *No description.* |
212 | | helmExtraValues
| any
| *No description.* |
213 | | namespace
| string
| *No description.* |
214 | | nodeRole
| aws-cdk-lib.aws_iam.Role
| *No description.* |
215 | | serviceAccountName
| string
| *No description.* |
216 | | version
| string
| *No description.* |
217 | | helmChartValues
| {[ key: string ]: any}
| *No description.* |
218 |
219 | ---
220 |
221 | ##### `node`Required
222 |
223 | ```typescript
224 | public readonly node: Node;
225 | ```
226 |
227 | - *Type:* constructs.Node
228 |
229 | The tree node.
230 |
231 | ---
232 |
233 | ##### `cluster`Required
234 |
235 | ```typescript
236 | public readonly cluster: Cluster;
237 | ```
238 |
239 | - *Type:* aws-cdk-lib.aws_eks.Cluster
240 |
241 | ---
242 |
243 | ##### `helmExtraValues`Required
244 |
245 | ```typescript
246 | public readonly helmExtraValues: any;
247 | ```
248 |
249 | - *Type:* any
250 |
251 | ---
252 |
253 | ##### `namespace`Required
254 |
255 | ```typescript
256 | public readonly namespace: string;
257 | ```
258 |
259 | - *Type:* string
260 |
261 | ---
262 |
263 | ##### `nodeRole`Required
264 |
265 | ```typescript
266 | public readonly nodeRole: Role;
267 | ```
268 |
269 | - *Type:* aws-cdk-lib.aws_iam.Role
270 |
271 | ---
272 |
273 | ##### `serviceAccountName`Required
274 |
275 | ```typescript
276 | public readonly serviceAccountName: string;
277 | ```
278 |
279 | - *Type:* string
280 |
281 | ---
282 |
283 | ##### `version`Required
284 |
285 | ```typescript
286 | public readonly version: string;
287 | ```
288 |
289 | - *Type:* string
290 |
291 | ---
292 |
293 | ##### `helmChartValues`Required
294 |
295 | ```typescript
296 | public readonly helmChartValues: {[ key: string ]: any};
297 | ```
298 |
299 | - *Type:* {[ key: string ]: any}
300 |
301 | ---
302 |
303 |
304 | ## Structs
305 |
306 | ### KarpenterProps
307 |
308 | #### Initializer
309 |
310 | ```typescript
311 | import { KarpenterProps } from 'cdk-eks-karpenter'
312 |
313 | const karpenterProps: KarpenterProps = { ... }
314 | ```
315 |
316 | #### Properties
317 |
318 | | **Name** | **Type** | **Description** |
319 | | --- | --- | --- |
320 | | cluster
| aws-cdk-lib.aws_eks.Cluster
| The EKS Cluster to attach to. |
321 | | version
| string
| The helm chart version to install. |
322 | | helmExtraValues
| {[ key: string ]: any}
| Extra values to pass to the Karpenter Helm chart. |
323 | | namespace
| string
| The Kubernetes namespace to install to. |
324 | | nodeRole
| aws-cdk-lib.aws_iam.Role
| Custom NodeRole to pass for Karpenter Nodes. |
325 | | serviceAccountName
| string
| The Kubernetes ServiceAccount name to use. |
326 |
327 | ---
328 |
329 | ##### `cluster`Required
330 |
331 | ```typescript
332 | public readonly cluster: Cluster;
333 | ```
334 |
335 | - *Type:* aws-cdk-lib.aws_eks.Cluster
336 |
337 | The EKS Cluster to attach to.
338 |
339 | ---
340 |
341 | ##### `version`Required
342 |
343 | ```typescript
344 | public readonly version: string;
345 | ```
346 |
347 | - *Type:* string
348 | - *Default:* latest
349 |
350 | The helm chart version to install.
351 |
352 | ---
353 |
354 | ##### `helmExtraValues`Optional
355 |
356 | ```typescript
357 | public readonly helmExtraValues: {[ key: string ]: any};
358 | ```
359 |
360 | - *Type:* {[ key: string ]: any}
361 |
362 | Extra values to pass to the Karpenter Helm chart.
363 |
364 | ---
365 |
366 | ##### `namespace`Optional
367 |
368 | ```typescript
369 | public readonly namespace: string;
370 | ```
371 |
372 | - *Type:* string
373 | - *Default:* karpenter
374 |
375 | The Kubernetes namespace to install to.
376 |
377 | ---
378 |
379 | ##### `nodeRole`Optional
380 |
381 | ```typescript
382 | public readonly nodeRole: Role;
383 | ```
384 |
385 | - *Type:* aws-cdk-lib.aws_iam.Role
386 |
387 | Custom NodeRole to pass for Karpenter Nodes.
388 |
389 | ---
390 |
391 | ##### `serviceAccountName`Optional
392 |
393 | ```typescript
394 | public readonly serviceAccountName: string;
395 | ```
396 |
397 | - *Type:* string
398 | - *Default:* karpenter
399 |
400 | The Kubernetes ServiceAccount name to use.
401 |
402 | ---
403 |
404 |
405 |
406 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
5 | opensource-codeofconduct@amazon.com with any additional questions or comments.
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature,
4 | correction, or additional documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all
7 | the necessary information to effectively respond to your bug report or contribution.
8 |
9 | ## Reporting Bugs/Feature Requests
10 |
11 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
12 |
13 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else
14 | hasn't already reported the issue. Please try to include as much information as you can. Details like
15 | these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 | ## Contributing via Pull Requests
23 |
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure
25 | that:
26 |
27 | 1. You are working against the latest source on the *main* branch.
28 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed
29 | the problem already.
30 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
31 |
32 | To send us a pull request, please:
33 |
34 | 1. Fork the repository.
35 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat
36 | all the code, it will be hard for us to focus on your change.
37 | 3. Ensure local tests pass.
38 | 4. Commit to your fork using clear commit messages.
39 | 5. Send us a pull request, answering any default questions in the pull request interface.
40 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the
41 | conversation.
42 |
43 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/)
44 | and [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
45 |
46 | ## Finding contributions to work on
47 |
48 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by
49 | default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix),
50 | looking at any 'help wanted' issues is a great place to start.
51 |
52 | ## Code of Conduct
53 |
54 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
55 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
56 | opensource-codeofconduct@amazon.com with any additional questions or comments.
57 |
58 | ## Security issue notifications
59 |
60 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security
61 | via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/).
62 | Please do **not** create a public github issue.
63 |
64 | ## Licensing
65 |
66 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
67 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # cdk-eks-karpenter
2 |
3 | This construct configures the necessary dependencies and installs [Karpenter](https://karpenter.sh)
4 | on an EKS cluster managed by AWS CDK.
5 |
6 | ## Prerequisites
7 |
8 | ### Usage with EC2 Spot Capacity
9 |
10 | If you have not used EC2 spot in your AWS account before, follow the instructions
11 | [here](https://karpenter.sh/v0.31/getting-started/getting-started-with-karpenter/#3-create-a-cluster) to create
12 | the service linked role in your account allowing Karpenter to provision EC2 Spot Capacity.
13 |
14 | ## Using
15 |
16 | In your CDK project, initialize a new Karpenter construct for your EKS cluster, like this:
17 |
18 | ```typescript
19 | const cluster = new Cluster(this, 'testCluster', {
20 | vpc: vpc,
21 | role: clusterRole,
22 | version: KubernetesVersion.V1_27,
23 | defaultCapacity: 1
24 | });
25 |
26 | const karpenter = new Karpenter(this, 'Karpenter', {
27 | cluster: cluster,
28 | namespace: "kube-system"
29 | });
30 | ```
31 |
32 | This will install and configure Karpenter in your cluster. To have Karpenter do something useful, you
33 | also need to create an [EC2NodeClass](https://karpenter.sh/docs/concepts/nodeclasses/) and an
34 | [NodePool](https://karpenter.sh/docs/concepts/nodepools/), for a more complete example, see
35 | [test/integ.karpenter.ts](./test/integ.karpenter.ts).
36 |
37 | ## Known issues
38 |
39 | ### It is now a best practice to install Karpenter into the kube-system namespace:
40 | Kapenter CRD webhooks have 'kube-system' hard-coded into them, and do not work in other namespaces (such as 'karpenter')
41 |
42 | ### Versions earlier than v0.6.1 fails to install
43 |
44 | As of [aws/karpenter#1145](https://github.com/aws/karpenter/pull/1145) the Karpenter Helm chart is
45 | refactored to specify `clusterEndpoint` and `clusterName` on the root level of the chart values, previously
46 | these values was specified under the key `controller`.
47 |
48 | ## Testing
49 |
50 | This construct adds a custom task to [projen](https://projen.io/), so you can test a full deployment
51 | of an EKS cluster with Karpenter installed as specified in `test/integ.karpenter.ts` by running the
52 | following:
53 |
54 | ```sh
55 | export CDK_DEFAULT_REGION=
56 | export CDK_DEFAULT_ACCOUNT=
57 | npx projen test:deploy
58 | ```
59 |
60 | As the above will create a cluster without EC2 capacity, with CoreDNS and Karpenter running as Fargate
61 | pods, you can test out the functionality of Karpenter by deploying an inflation deployment, which will
62 | spin up a number of pods that will trigger Karpenter creation of worker nodes:
63 |
64 | ```sh
65 | kubectl apply -f test/inflater-deployment.yml
66 | ```
67 |
68 | You can clean things up by deleting the deployment and the CDK test stack:
69 |
70 | ```sh
71 | kubectl delete -f test/inflater-deployment.yml
72 | npx projen test:destroy
73 | ```
74 |
75 | ## FAQ
76 |
77 | ### I'm not able to launch spot instances
78 |
79 | 1. Ensure you have the appropriate linked role available in your account, for more details,
80 | see [the karpenter documentation](https://karpenter.sh/v0.31/getting-started/getting-started-with-karpenter/#3-create-a-cluster)
81 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cdk-eks-karpenter",
3 | "description": "CDK construct library that allows you install Karpenter in an AWS EKS cluster",
4 | "repository": {
5 | "type": "git",
6 | "url": "https://github.com/aws-samples/cdk-eks-karpenter.git"
7 | },
8 | "scripts": {
9 | "build": "npx projen build",
10 | "bump": "npx projen bump",
11 | "clobber": "npx projen clobber",
12 | "compat": "npx projen compat",
13 | "compile": "npx projen compile",
14 | "default": "npx projen default",
15 | "docgen": "npx projen docgen",
16 | "eject": "npx projen eject",
17 | "eslint": "npx projen eslint",
18 | "package": "npx projen package",
19 | "package-all": "npx projen package-all",
20 | "package:js": "npx projen package:js",
21 | "package:python": "npx projen package:python",
22 | "post-compile": "npx projen post-compile",
23 | "pre-compile": "npx projen pre-compile",
24 | "release": "npx projen release",
25 | "test": "npx projen test",
26 | "test:deploy": "npx projen test:deploy",
27 | "test:destroy": "npx projen test:destroy",
28 | "test:synth": "npx projen test:synth",
29 | "test:watch": "npx projen test:watch",
30 | "unbump": "npx projen unbump",
31 | "watch": "npx projen watch",
32 | "projen": "npx projen"
33 | },
34 | "author": {
35 | "name": "Andreas Lindh",
36 | "email": "elindh@amazon.com",
37 | "organization": false
38 | },
39 | "devDependencies": {
40 | "@aws-cdk/lambda-layer-kubectl-v24": "^2.0.242",
41 | "@aws-cdk/lambda-layer-kubectl-v25": "^2.0.4",
42 | "@aws-cdk/lambda-layer-kubectl-v26": "^2.0.1",
43 | "@aws-cdk/lambda-layer-kubectl-v27": "^2.0.0",
44 | "@aws-cdk/lambda-layer-kubectl-v28": "^2.2.0",
45 | "@aws-cdk/lambda-layer-kubectl-v29": "^2.1.0",
46 | "@aws-cdk/lambda-layer-kubectl-v30": "^2.0.1",
47 | "@aws-cdk/lambda-layer-kubectl-v31": "^2.0.0",
48 | "@types/jest": "^27",
49 | "@types/node": "^16",
50 | "@typescript-eslint/eslint-plugin": "^6",
51 | "@typescript-eslint/parser": "^6",
52 | "aws-cdk-lib": "2.178.2",
53 | "constructs": "10.0.5",
54 | "eslint": "^8",
55 | "eslint-import-resolver-node": "^0.3.6",
56 | "eslint-import-resolver-typescript": "^2.5.0",
57 | "eslint-plugin-import": "^2.25.4",
58 | "jest": "^27",
59 | "jest-junit": "^15",
60 | "jsii": "1.x",
61 | "jsii-diff": "^1.55.1",
62 | "jsii-docgen": "^6.2.3",
63 | "jsii-pacmak": "1.104.0",
64 | "jsii-rosetta": "1.x",
65 | "projen": "^0.76.17",
66 | "standard-version": "^9",
67 | "ts-jest": "^27",
68 | "typescript": "^4.6.2"
69 | },
70 | "peerDependencies": {
71 | "aws-cdk-lib": "^2.178.2",
72 | "constructs": "^10.0.5"
73 | },
74 | "dependencies": {
75 | "semver": "^7.5.4"
76 | },
77 | "bundledDependencies": [
78 | "semver"
79 | ],
80 | "resolutions": {
81 | "@types/babel__traverse": "7.18.2",
82 | "@types/prettier": "2.6.0"
83 | },
84 | "keywords": [
85 | "cdk",
86 | "eks",
87 | "karpenter"
88 | ],
89 | "main": "lib/index.js",
90 | "license": "Apache-2.0",
91 | "version": "0.0.0",
92 | "jest": {
93 | "testMatch": [
94 | "/src/**/__tests__/**/*.ts?(x)",
95 | "/(test|src)/**/*(*.)@(spec|test).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 | "preset": "ts-jest",
126 | "globals": {
127 | "ts-jest": {
128 | "tsconfig": "tsconfig.dev.json"
129 | }
130 | }
131 | },
132 | "types": "lib/index.d.ts",
133 | "stability": "stable",
134 | "jsii": {
135 | "outdir": "dist",
136 | "targets": {
137 | "python": {
138 | "distName": "cdk-eks-karpenter",
139 | "module": "cdk_eks_karpenter"
140 | }
141 | },
142 | "tsc": {
143 | "outDir": "lib",
144 | "rootDir": "src"
145 | }
146 | },
147 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"."
148 | }
149 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import { Aws, CfnJson, Duration } from 'aws-cdk-lib';
2 | import { Cluster, HelmChart } from 'aws-cdk-lib/aws-eks';
3 | import { Rule } from 'aws-cdk-lib/aws-events';
4 | import { SqsQueue } from 'aws-cdk-lib/aws-events-targets';
5 | import { CfnInstanceProfile, IManagedPolicy, ManagedPolicy, Policy, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
6 | import { IQueue, Queue } from 'aws-cdk-lib/aws-sqs';
7 | import { Construct } from 'constructs';
8 | import * as semver from 'semver';
9 | import { Utils } from './utils';
10 |
11 | export interface KarpenterProps {
12 | /**
13 | * The EKS Cluster to attach to
14 | */
15 | readonly cluster: Cluster;
16 |
17 | /**
18 | * The Kubernetes namespace to install to
19 | *
20 | * @default karpenter
21 | */
22 | readonly namespace?: string;
23 |
24 | /**
25 | * The Kubernetes ServiceAccount name to use
26 | *
27 | * @default karpenter
28 | */
29 | readonly serviceAccountName?: string;
30 |
31 | /**
32 | * The helm chart version to install
33 | *
34 | * @default - latest
35 | */
36 | readonly version: string;
37 |
38 | /**
39 | * Extra values to pass to the Karpenter Helm chart
40 | */
41 | readonly helmExtraValues?: Record;
42 |
43 | /**
44 | * Custom NodeRole to pass for Karpenter Nodes
45 | */
46 | readonly nodeRole?: Role;
47 | }
48 |
49 | export class Karpenter extends Construct {
50 | public readonly cluster: Cluster;
51 | public readonly namespace: string;
52 | public readonly serviceAccountName: string;
53 | public readonly version: string;
54 | public readonly nodeRole: Role;
55 | public readonly helmExtraValues: any;
56 |
57 | private readonly chart: HelmChart;
58 | private readonly serviceAccount: any;
59 | public helmChartValues: Record;
60 | private controllerIAMPolicyStatements: PolicyStatement[];
61 | private interruptionQueue: IQueue | undefined;
62 |
63 | constructor(scope: Construct, id: string, props: KarpenterProps) {
64 | super(scope, id);
65 |
66 | this.cluster = props.cluster;
67 | this.namespace = props.namespace ?? 'karpenter';
68 | this.serviceAccountName = props.serviceAccountName ?? 'karpenter';
69 | this.version = props.version;
70 | this.helmExtraValues = props.helmExtraValues ?? {};
71 |
72 | this.controllerIAMPolicyStatements = [];
73 | this.interruptionQueue = undefined;
74 |
75 | this.helmChartValues = {
76 | settings: {
77 | aws: {},
78 | },
79 | };
80 |
81 | /*
82 | * We create a node role for Karpenter managed nodes, alongside an instance profile for the EC2
83 | * instances that will be managed by karpenter.
84 | *
85 | * We will also create a role mapping in the `aws-auth` ConfigMap so that the nodes can authenticate
86 | * with the Kubernetes API using IAM.
87 | *
88 | * Create Node Role if nodeRole not added as prop
89 | * Make sure that the Role that is added does not have an Instance Profile associated to it
90 | * since we will create it here.
91 | */
92 | if (!props.nodeRole) {
93 | this.nodeRole = new Role(this, 'NodeRole', {
94 | assumedBy: new ServicePrincipal(`ec2.${Aws.URL_SUFFIX}`),
95 | managedPolicies: [
96 | ManagedPolicy.fromAwsManagedPolicyName('AmazonEKS_CNI_Policy'),
97 | ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSWorkerNodePolicy'),
98 | ManagedPolicy.fromAwsManagedPolicyName('AmazonEC2ContainerRegistryReadOnly'),
99 | ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'),
100 | ],
101 | });
102 | } else {
103 | this.nodeRole = props.nodeRole;
104 | }
105 |
106 |
107 | const instanceProfile = new CfnInstanceProfile(this, 'InstanceProfile', {
108 | roles: [this.nodeRole.roleName],
109 | });
110 |
111 | this.cluster.awsAuth.addRoleMapping(this.nodeRole, {
112 | username: 'system:node:{{EC2PrivateDNSName}}',
113 | groups: [
114 | 'system:bootstrappers',
115 | 'system:nodes',
116 | ],
117 | });
118 |
119 | /**
120 | * For the Karpenter controller to be able to talk to the AWS APIs, we need to set up a few
121 | * resources which will allow the Karpenter controller to use IAM Roles for Service Accounts
122 | */
123 |
124 | this.serviceAccount = this.cluster.addServiceAccount('karpenter', {
125 | name: this.serviceAccountName,
126 | namespace: this.namespace,
127 | });
128 |
129 |
130 | // Setup the controller IAM Policy statements
131 | this.addControllerPolicyIAMPolicyStatements();
132 |
133 | // Set repoUrl based on which version of Karpenter we want to install
134 | const repoUrl = this.helmRepoURLFromKarpenterVersion();
135 |
136 | // Setup the interruption queue, different helm chart versions have different key names
137 | this.interruptionQueue = this.addInterruptionQueue();
138 |
139 | // Manage different helm values depending on Karpenter version
140 | if (semver.gte(this.version, 'v0.19.0') && semver.lte(this.version, 'v0.32.0')) {
141 | this.helmChartValues.settings.aws = {
142 | clusterName: this.cluster.clusterName,
143 | clusterEndpoint: this.cluster.clusterEndpoint,
144 | defaultInstanceProfile: instanceProfile.ref,
145 | interruptionQueueName: this.interruptionQueue?.queueName,
146 | };
147 | }
148 | if (semver.gte(this.version, 'v0.32.0')) {
149 | this.helmChartValues.settings = {
150 | clusterName: this.cluster.clusterName,
151 | clusterEndpoint: this.cluster.clusterEndpoint,
152 | interruptionQueue: this.interruptionQueue?.queueName,
153 | };
154 | }
155 |
156 | // These are fixed values that we supply to the Helm Chart.
157 | this.helmChartValues = {
158 | ...{
159 | serviceAccount: {
160 | create: false,
161 | name: this.serviceAccount.serviceAccountName,
162 | annotations: {
163 | 'eks.amazonaws.com/role-arn': this.serviceAccount.role.roleArn,
164 | },
165 | },
166 | },
167 | ...this.helmChartValues,
168 | };
169 |
170 | new Policy(this, 'ControllerPolicy', {
171 | roles: [this.serviceAccount.role],
172 | statements: this.controllerIAMPolicyStatements,
173 | });
174 |
175 | /**
176 | * Finally, we can go ahead and install the Helm chart provided for Karpenter with the inputs
177 | * we desire.
178 | */
179 | this.chart = this.cluster.addHelmChart('karpenter', {
180 | // This one is important, if we don't ask helm to wait for resources to become available, the
181 | // subsequent creation of karpenter resources will fail.
182 | wait: true,
183 | chart: 'karpenter',
184 | release: 'karpenter',
185 | repository: repoUrl,
186 | namespace: this.namespace,
187 | version: this.version,
188 | createNamespace: false,
189 | // We will merge our dyanmic `helmExtraValues` with the fixed values. Where the fixed values
190 | // will override the dynamic values.
191 | values: { ...this.helmExtraValues, ...this.helmChartValues },
192 | });
193 |
194 |
195 | // If we are not installing it in the `kube-system` namespace:
196 | // Note: We should be installing it in kube-system, please see: https://github.com/aws/karpenter-provider-aws/blob/fd2b60759f81dc0c868810cc44443103067c4880/website/content/en/v0.36/upgrading/upgrade-guide.md?plain=1#L91
197 | // Also see https://github.com/aws-samples/cdk-eks-karpenter/issues/189 and https://github.com/aws-samples/cdk-eks-karpenter/issues/173
198 | if (this.namespace != 'kube-system') {
199 | const namespace = this.cluster.addManifest('karpenter-namespace', {
200 | apiVersion: 'v1',
201 | kind: 'Namespace',
202 | metadata: {
203 | name: this.namespace,
204 | },
205 | });
206 | // If we are creating a namespace, we need to link it to the service account and the chart, so they are deployed in the correct order.
207 | this.serviceAccount.node.addDependency(namespace);
208 | this.chart.node.addDependency(namespace);
209 | }
210 | }
211 |
212 | /**
213 | * addEC2NodeClass adds a EC2NodeClass to the Karpenter configuration.
214 | *
215 | * @param id must consist of lower case alphanumeric characters, \'-\' or \'.\', and must start and end with an alphanumeric character
216 | * @param ec2NodeClassSpec spec of Karpenters EC2NodeClass API
217 | *
218 | * @returns the metadata object of the created manifest
219 | */
220 | public addEC2NodeClass(id: string, ec2NodeClassSpec: Record): Record {
221 | // Validate the name of the resource
222 | if (!Utils.validateKubernetesNameConformance(id)) {
223 | throw new Error('name does not conform to k8s policy');
224 | }
225 |
226 | // Ensure we provide a valid spec
227 | Utils.hasRequiredKeys(ec2NodeClassSpec, [
228 | 'amiFamily', 'subnetSelectorTerms', 'securityGroupSelectorTerms', 'role',
229 | ]);
230 |
231 | return this.addManifest(id, {
232 | apiVersion: 'karpenter.k8s.aws/v1',
233 | kind: 'EC2NodeClass',
234 | metadata: {
235 | name: id,
236 | namespace: this.namespace,
237 | },
238 | spec: ec2NodeClassSpec,
239 | });
240 | }
241 |
242 | /**
243 | * addNodePool adds a NodePool to the Karpenter configuration.
244 | *
245 | * @param id must consist of lower case alphanumeric characters, \'-\' or \'.\', and must start and end with an alphanumeric character
246 | * @param nodePoolSpec spec of Karpenters NodePool API
247 | *
248 | * @returns the metadata object of the created manifest
249 | */
250 | public addNodePool(id: string, nodePoolSpec: Record): Record {
251 | // Validate the name of the resource
252 | if (!Utils.validateKubernetesNameConformance(id)) {
253 | throw new Error('name does not conform to k8s policy');
254 | }
255 |
256 | // Ensure we provide a valid spec
257 | Utils.hasRequiredKeys(nodePoolSpec.template.spec, ['nodeClassRef', 'requirements']);
258 |
259 | return this.addManifest(id, {
260 | apiVersion: 'karpenter.sh/v1',
261 | kind: 'NodePool',
262 | metadata: {
263 | name: id,
264 | namespace: this.namespace,
265 | },
266 | spec: nodePoolSpec,
267 | });
268 | }
269 |
270 | /**
271 | * addProvisioner adds a provisioner manifest to the cluster. Currently the provisioner spec
272 | * parameter is relatively free form.
273 | *
274 | * @param id - must consist of lower case alphanumeric characters, \'-\' or \'.\', and must start and end with an alphanumeric character
275 | * @param provisionerSpec - spec of Karpenters Provisioner object.
276 | *
277 | * @deprecated This method should not be used with Karpenter >v0.32.0
278 | */
279 | public addProvisioner(id: string, provisionerSpec: Record): void {
280 | // Validate the name of the resource
281 | if (!Utils.validateKubernetesNameConformance(id)) {
282 | throw new Error('name does not conform to k8s policy');
283 | }
284 | // If later than version v0.32.0, we should throw an exception here as the APIs
285 | // changed after that version and this method should not be used.
286 | if (semver.gte('v0.32.0', this.version)) {
287 | throw new Error('This method is not supported for this Karpenter version. Please use addEC2NodeClass instead.');
288 | }
289 | this.addManifest(id, {
290 | apiVersion: 'karpenter.k8s.aws/v1alpha5',
291 | kind: 'Provisioner',
292 | metadata: {
293 | name: id,
294 | namespace: this.namespace,
295 | },
296 | spec: provisionerSpec,
297 | });
298 | }
299 |
300 | /**
301 | * addNodeTemplate adds a node template manifest to the cluster. Currently the node template spec
302 | * parameter is relatively free form.
303 | *
304 | * @param id - must consist of lower case alphanumeric characters, \'-\' or \'.\', and must start and end with an alphanumeric character
305 | * @param nodeTemplateSpec - spec of Karpenters Node Template object.
306 | *
307 | * @deprecated This method should not be used with Karpenter >v0.32.0
308 | */
309 | public addNodeTemplate(id: string, nodeTemplateSpec: Record): void {
310 | // Validate the name of the resource
311 | if (!Utils.validateKubernetesNameConformance(id)) {
312 | throw new Error('name does not conform to k8s policy');
313 | }
314 |
315 | // If version >= v0.32.0, we should throw an exception here as the APIs
316 | // changed after that version and this method should not be used.
317 | if (semver.gte('v0.32.0', this.version)) {
318 | throw new Error('This method is not supported for this Karpenter version. Please use addEC2NodeClass instead.');
319 | }
320 | this.addManifest(id, {
321 | apiVersion: 'karpenter.k8s.aws/v1',
322 | kind: 'AWSNodeTemplate',
323 | metadata: {
324 | namespace: this.namespace,
325 | },
326 | spec: nodeTemplateSpec,
327 | });
328 | }
329 |
330 | /**
331 | * addManifest crafts Kubernetes manifests for the specific APIs
332 | *
333 | * @param id
334 | * @param apiVersion
335 | * @param kind
336 | * @param metadata
337 | * @param spec
338 | *
339 | * @returns the metadata object of the created manifest
340 | */
341 | private addManifest(
342 | id: string,
343 | props: {
344 | apiVersion: string;
345 | kind: string;
346 | metadata: Record;
347 | spec: Record;
348 | },
349 | ): Record {
350 | let defaultMetadata: Record = {
351 | name: id,
352 | };
353 | let m = {
354 | apiVersion: props.apiVersion,
355 | kind: props.kind,
356 | metadata: {
357 | // We will merge our metadata details. The parameters provided will be overwritten by
358 | // defaultMetadata
359 | ...props.metadata, ...defaultMetadata,
360 | },
361 | spec: props.spec,
362 | };
363 | let manifstResource = this.cluster.addManifest(id, m);
364 | manifstResource.node.addDependency(this.chart);
365 |
366 | return m.metadata;
367 | }
368 |
369 | /**
370 | * addManagedPolicyToKarpenterRole adds Managed Policies To Karpenter Role.
371 | *
372 | * @param managedPolicy - iam managed policy to add to the karpenter role.
373 | */
374 | public addManagedPolicyToKarpenterRole(managedPolicy: IManagedPolicy): void {
375 | this.serviceAccount.role.addManagedPolicy(managedPolicy);
376 | }
377 |
378 | /**
379 | * Get the Helm repo URL based on the Karpenter version
380 | *
381 | * @returns string
382 | */
383 | private helmRepoURLFromKarpenterVersion(): string {
384 | if (semver.satisfies(this.version, '>= v0.17.0')) {
385 | return 'oci://public.ecr.aws/karpenter/karpenter';
386 | }
387 |
388 | return 'https://charts.karpenter.sh';
389 | }
390 |
391 | /**
392 | * addInterruptionQueue adds the interruption queue setup if neceesary
393 | */
394 | private addInterruptionQueue(): IQueue | undefined {
395 | if (semver.lte(this.version, 'v0.19.0')) {
396 | return undefined;
397 | };
398 |
399 | // new version need SQS to handle the interruption
400 | const interruptionQueue = new Queue(this, 'KarpenterInterruptionQueue', {
401 | queueName: this.cluster.clusterName,
402 | retentionPeriod: Duration.minutes(5),
403 | });
404 |
405 | const rules = [
406 | // ScheduledChangeRule
407 | new Rule(this, 'ScheduledChangeRule', {
408 | eventPattern: {
409 | source: ['aws.health'],
410 | detailType: ['AWS Health Event'],
411 | },
412 | }),
413 | // SpotInterruptionRule
414 | new Rule(this, 'SpotInterruptionRule', {
415 | eventPattern: {
416 | source: ['aws.ec2'],
417 | detailType: ['EC2 Spot Instance Interruption Warning'],
418 | },
419 | }),
420 | // RebalanceRule
421 | new Rule(this, 'RebalanceRule', {
422 | eventPattern: {
423 | source: ['aws.ec2'],
424 | detailType: ['EC2 Instance Rebalance Recommendation'],
425 | },
426 | }),
427 | // InstanceStateChangeRule
428 | new Rule(this, 'InstanceStateChangeRule', {
429 | eventPattern: {
430 | source: ['aws.ec2'],
431 | detailType: ['EC2 Instance State-change Notification'],
432 | },
433 | }),
434 | ];
435 |
436 | for (var rule of rules) {
437 | rule.addTarget(new SqsQueue(interruptionQueue));
438 | }
439 |
440 | // new version need SQS privilege
441 | this.controllerIAMPolicyStatements.push(
442 | new PolicyStatement({
443 | actions: [
444 | 'sqs:DeleteMessage',
445 | 'sqs:GetQueueAttributes',
446 | 'sqs:GetQueueUrl',
447 | 'sqs:ReceiveMessage',
448 | ],
449 | resources: [interruptionQueue.queueArn],
450 | }),
451 | );
452 |
453 | return interruptionQueue;
454 | }
455 |
456 | /**
457 | * Configure the IAM Policy StatementsPolicies for the Controller
458 | * taken from https://raw.githubusercontent.com/aws/karpenter/v0.32.0/website/content/en/preview/getting-started/getting-started-with-karpenter/cloudformation.yaml
459 | */
460 | private addControllerPolicyIAMPolicyStatements(): void {
461 | const clusterName = this.cluster.clusterName;
462 | const region = Aws.REGION;
463 | const partition = Aws.PARTITION;
464 | const accountId = Aws.ACCOUNT_ID;
465 |
466 | const conditionCfnJson = (id: string, value: Record) => new CfnJson(this, id, { value: value });
467 |
468 | this.controllerIAMPolicyStatements.push(
469 | new PolicyStatement({
470 | sid: 'AllowScopedEC2InstanceAccessActions',
471 | resources: [
472 | `arn:${partition}:ec2:${region}::image/*`,
473 | `arn:${partition}:ec2:${region}::snapshot/*`,
474 | `arn:${partition}:ec2:${region}:*:security-group/*`,
475 | `arn:${partition}:ec2:${region}:*:subnet/*`,
476 | ],
477 | actions: ['ec2:RunInstances', 'ec2:CreateFleet'],
478 | }),
479 | new PolicyStatement({
480 | sid: 'AllowScopedEC2LaunchTemplateAccessActions',
481 | resources: [`arn:${partition}:ec2:${region}:*:launch-template/*`],
482 | actions: ['ec2:RunInstances', 'ec2:CreateFleet'],
483 | conditions: {
484 | StringEquals: conditionCfnJson('AllowScopedEC2InstanceAccessActions-ResourceTagClusterName', {
485 | [`aws:ResourceTag/kubernetes.io/cluster/${clusterName}`]: 'owned',
486 | }),
487 | StringLike: conditionCfnJson('AllowScopedEC2InstanceAccessActions-ResourceTagNodePool', {
488 | 'aws:ResourceTag/karpenter.sh/nodepool': '*',
489 | }),
490 | },
491 | }),
492 | new PolicyStatement({
493 | sid: 'AllowScopedEC2InstanceActionsWithTags',
494 | resources: [
495 | `arn:${partition}:ec2:${region}:*:fleet/*`,
496 | `arn:${partition}:ec2:${region}:*:instance/*`,
497 | `arn:${partition}:ec2:${region}:*:volume/*`,
498 | `arn:${partition}:ec2:${region}:*:network-interface/*`,
499 | `arn:${partition}:ec2:${region}:*:launch-template/*`,
500 | `arn:${partition}:ec2:${region}:*:spot-instances-request/*`,
501 | ],
502 | actions: ['ec2:RunInstances', 'ec2:CreateFleet', 'ec2:CreateLaunchTemplate'],
503 | conditions: {
504 | StringEquals: conditionCfnJson('AllowScopedEC2InstanceActionsWithTags-1', {
505 | [`aws:RequestTag/kubernetes.io/cluster/${clusterName}`]: 'owned',
506 | 'aws:RequestTag/eks:eks-cluster-name': clusterName,
507 | }),
508 | StringLike: conditionCfnJson('AllowScopedEC2InstanceActionsWithTags-2', {
509 | 'aws:RequestTag/karpenter.sh/nodepool': '*',
510 | }),
511 | },
512 | }),
513 | new PolicyStatement({
514 | sid: 'AllowScopedResourceCreationTagging',
515 | resources: [
516 | `arn:${partition}:ec2:${region}:*:fleet/*`,
517 | `arn:${partition}:ec2:${region}:*:instance/*`,
518 | `arn:${partition}:ec2:${region}:*:volume/*`,
519 | `arn:${partition}:ec2:${region}:*:network-interface/*`,
520 | `arn:${partition}:ec2:${region}:*:launch-template/*`,
521 | `arn:${partition}:ec2:${region}:*:spot-instances-request/*`,
522 | ],
523 | actions: ['ec2:CreateTags'],
524 | conditions: {
525 | StringEquals: conditionCfnJson('AllowScopedResourceCreationTagging-1', {
526 | [`aws:RequestTag/kubernetes.io/cluster/${clusterName}`]: 'owned',
527 | 'aws:RequestTag/eks:eks-cluster-name': clusterName,
528 | 'ec2:CreateAction': ['RunInstances', 'CreateFleet', 'CreateLaunchTemplate'],
529 | }),
530 | StringLike: conditionCfnJson('AllowScopedResourceCreationTagging-2', {
531 | 'aws:RequestTag/karpenter.sh/nodepool': '*',
532 | }),
533 | },
534 | }),
535 | new PolicyStatement({
536 | sid: 'AllowScopedResourceTagging',
537 | resources: [`arn:${partition}:ec2:${region}:*:instance/*`],
538 | actions: ['ec2:CreateTags'],
539 | conditions: {
540 | 'StringEquals': conditionCfnJson('AllowScopedResourceTagging-1', {
541 | [`aws:ResourceTag/kubernetes.io/cluster/${clusterName}`]: 'owned',
542 | }),
543 | 'StringLike': conditionCfnJson('AllowScopedResourceTagging-2', {
544 | 'aws:ResourceTag/karpenter.sh/nodepool': '*',
545 | }),
546 | 'StringEqualsIfExists': conditionCfnJson('AllowScopedResourceTagging-3', {
547 | 'aws:RequestTag/eks:eks-cluster-name': clusterName,
548 | }),
549 | 'ForAllValues:StringEquals': conditionCfnJson('AllowScopedResourceTagging-4', {
550 | 'aws:TagKeys': ['eks:eks-cluster-name', 'karpenter.sh/nodeclaim', 'Name'],
551 | }),
552 | },
553 | }),
554 | new PolicyStatement({
555 | sid: 'AllowScopedDeletion',
556 | resources: [
557 | `arn:${partition}:ec2:${region}:*:instance/*`,
558 | `arn:${partition}:ec2:${region}:*:launch-template/*`,
559 | ],
560 | actions: ['ec2:TerminateInstances', 'ec2:DeleteLaunchTemplate'],
561 | conditions: {
562 | StringEquals: conditionCfnJson('AllowScopedDeletion-1', {
563 | [`aws:ResourceTag/kubernetes.io/cluster/${clusterName}`]: 'owned',
564 | }),
565 | StringLike: conditionCfnJson('AllowScopedDeletion-2', {
566 | 'aws:ResourceTag/karpenter.sh/nodepool': '*',
567 | }),
568 | },
569 | }),
570 | new PolicyStatement({
571 | sid: 'AllowRegionalReadActions',
572 | resources: ['*'],
573 | actions: [
574 | 'ec2:DescribeImages',
575 | 'ec2:DescribeInstances',
576 | 'ec2:DescribeInstanceTypeOfferings',
577 | 'ec2:DescribeInstanceTypes',
578 | 'ec2:DescribeLaunchTemplates',
579 | 'ec2:DescribeSecurityGroups',
580 | 'ec2:DescribeSpotPriceHistory',
581 | 'ec2:DescribeSubnets',
582 | ],
583 | conditions: {
584 | StringEquals: {
585 | 'aws:RequestedRegion': region,
586 | },
587 | },
588 | }),
589 | new PolicyStatement({
590 | sid: 'AllowSSMReadActions',
591 | resources: [`arn:${partition}:ssm:${region}::parameter/aws/service/*`],
592 | actions: ['ssm:GetParameter'],
593 | }),
594 | new PolicyStatement({
595 | sid: 'AllowPricingReadActions',
596 | resources: ['*'],
597 | actions: ['pricing:GetProducts'],
598 | }),
599 | new PolicyStatement({
600 | sid: 'AllowPassingInstanceRole',
601 | resources: [this.nodeRole.roleArn],
602 | actions: ['iam:PassRole'],
603 | conditions: {
604 | StringEquals: {
605 | 'iam:PassedToService': ['ec2.amazonaws.com', 'ec2.amazonaws.com.cn'],
606 | },
607 | },
608 | }),
609 | new PolicyStatement({
610 | sid: 'AllowScopedInstanceProfileCreationActions',
611 | resources: [`arn:${partition}:iam::${accountId}:instance-profile/*`],
612 | actions: ['iam:CreateInstanceProfile'],
613 | conditions: {
614 | StringEquals: conditionCfnJson('AllowScopedInstanceProfileCreationActions-1', {
615 | [`aws:RequestTag/kubernetes.io/cluster/${clusterName}`]: 'owned',
616 | 'aws:RequestTag/eks:eks-cluster-name': clusterName,
617 | 'aws:RequestTag/topology.kubernetes.io/region': region,
618 | }),
619 | StringLike: conditionCfnJson('AllowScopedInstanceProfileCreationActions-2', {
620 | 'aws:RequestTag/karpenter.k8s.aws/ec2nodeclass': '*',
621 | }),
622 | },
623 | }),
624 | new PolicyStatement({
625 | sid: 'AllowScopedInstanceProfileTagActions',
626 | resources: [`arn:${partition}:iam::${accountId}:instance-profile/*`],
627 | actions: ['iam:TagInstanceProfile'],
628 | conditions: {
629 | StringEquals: conditionCfnJson('AllowScopedInstanceProfileTagActions-1', {
630 | [`aws:ResourceTag/kubernetes.io/cluster/${clusterName}`]: 'owned',
631 | 'aws:ResourceTag/topology.kubernetes.io/region': region,
632 | [`aws:RequestTag/kubernetes.io/cluster/${clusterName}`]: 'owned',
633 | 'aws:RequestTag/eks:eks-cluster-name': clusterName,
634 | 'aws:RequestTag/topology.kubernetes.io/region': region,
635 | }),
636 | StringLike: conditionCfnJson('AllowScopedInstanceProfileTagActions-2', {
637 | 'aws:ResourceTag/karpenter.k8s.aws/ec2nodeclass': '*',
638 | 'aws:RequestTag/karpenter.k8s.aws/ec2nodeclass': '*',
639 | }),
640 | },
641 | }));
642 | }
643 | }
644 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Utils should only contain static methods.
3 | */
4 | export class Utils {
5 | /**
6 | * Check whether a string conforms to the lowercase RFC 1123. If not, Kubernetes will throw
7 | * an error saying that the name must conform with regex used for validation, which is:
8 | * \'[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*\')\n'
9 | *
10 | * @param s string to validate
11 | *
12 | * @returns boolean
13 | */
14 | public static validateKubernetesNameConformance(s: string): boolean {
15 | let regex = new RegExp('^(?![0-9]+$)(?!.*-$)(?!-)[a-z0-9-]{1,63}$');
16 | return regex.test(s);
17 | }
18 |
19 | /**
20 | * Checks the object to ensure that all required keys are present, or throws an error.
21 | *
22 | * @param obj object to check
23 | * @param required list of strings to ensure presence
24 | *
25 | * @returns boolean
26 | */
27 | public static hasRequiredKeys(obj: Record, required: string[]): void {
28 | for (let key of required) {
29 | if (!obj.hasOwnProperty(key)) {
30 | throw new Error(`Missing required key: ${key}, full object: ${JSON.stringify(obj)}`);
31 | }
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/test/inflater-deployment.yml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: inflate
5 | spec:
6 | replicas: 8
7 | selector:
8 | matchLabels:
9 | app: inflate
10 | template:
11 | metadata:
12 | labels:
13 | app: inflate
14 | spec:
15 | terminationGracePeriodSeconds: 0
16 | containers:
17 | - name: inflate
18 | image: public.ecr.aws/eks-distro/kubernetes/pause:3.2
19 | resources:
20 | requests:
21 | cpu: 1
--------------------------------------------------------------------------------
/test/installation.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Template, Capture } from 'aws-cdk-lib/assertions';
3 | import { Cluster, KubernetesVersion } from 'aws-cdk-lib/aws-eks';
4 | import { Karpenter } from '../src';
5 |
6 | describe('Karpenter installation', () => {
7 | it('should install the desired version', () => {
8 | const app = new cdk.App();
9 | const stack = new cdk.Stack(app, 'test-stack');
10 |
11 | const cluster = new Cluster(stack, 'testcluster', {
12 | version: KubernetesVersion.V1_27,
13 | });
14 |
15 | // Create Karpenter install with non-default version
16 | new Karpenter(stack, 'Karpenter', {
17 | cluster: cluster,
18 | version: 'v0.6.0',
19 | });
20 |
21 | const t = Template.fromStack(stack);
22 | t.hasResource('Custom::AWSCDK-EKS-Cluster', {});
23 | t.hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', {
24 | Version: 'v0.6.0',
25 | });
26 | });
27 |
28 | it('should install in a different namespace', () => {
29 | const app = new cdk.App();
30 | const stack = new cdk.Stack(app, 'test-stack');
31 |
32 | const cluster = new Cluster(stack, 'testcluster', {
33 | version: KubernetesVersion.V1_27,
34 | });
35 |
36 | // Create Karpenter install with non-default namespace
37 | new Karpenter(stack, 'Karpenter', {
38 | cluster: cluster,
39 | namespace: 'kar-penter',
40 | version: 'v0.32.0',
41 | });
42 |
43 | const t = Template.fromStack(stack);
44 | t.hasResource('Custom::AWSCDK-EKS-Cluster', {});
45 | t.hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', {
46 | Namespace: 'kar-penter',
47 | });
48 | });
49 |
50 | it('should add extra helm values if provided', () => {
51 | const app = new cdk.App();
52 | const stack = new cdk.Stack(app, 'test-stack');
53 |
54 | const cluster = new Cluster(stack, 'testcluster', {
55 | version: KubernetesVersion.V1_27,
56 | });
57 |
58 | // Create Karpenter install with extra values
59 | new Karpenter(stack, 'Karpenter', {
60 | cluster: cluster,
61 | namespace: 'kar-penter',
62 | helmExtraValues: {
63 | 'foo.key': 'foo.value',
64 | },
65 | version: 'v0.32.0',
66 | });
67 |
68 | const t = Template.fromStack(stack);
69 | const valueCapture = new Capture();
70 | t.hasResource('Custom::AWSCDK-EKS-Cluster', {});
71 | t.hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', {
72 | Values: valueCapture,
73 | Namespace: 'kar-penter',
74 | });
75 |
76 | const values = valueCapture.asObject();
77 | expect(values['Fn::Join'][1][0]).toContain('{\"foo.key\":\"foo.value\"');
78 | });
79 |
80 | it('EC2NodeClass should fail invalid name with correct properties', () => {
81 | const app = new cdk.App();
82 | const stack = new cdk.Stack(app, 'test-stack');
83 |
84 | const cluster = new Cluster(stack, 'testcluster', {
85 | version: KubernetesVersion.V1_27,
86 | });
87 |
88 | const karpenter = new Karpenter(stack, 'Karpenter', {
89 | cluster: cluster,
90 | version: 'v0.32.0',
91 | });
92 |
93 | expect(
94 | () => karpenter.addEC2NodeClass('ec2NODECLASSINVALID', {
95 | amiFamily: 'AL2',
96 | subnetSelectorTerms: [
97 | {
98 | tags: {
99 | Name: 'sometagvalue',
100 | },
101 | },
102 | ],
103 | securityGroupSelectorTerms: [
104 | {
105 | tags: {
106 | 'aws:eks:cluster-name': cluster.clusterName,
107 | },
108 | },
109 | ],
110 | role: karpenter.nodeRole.roleName,
111 | }),
112 | ).toThrowError();
113 | });
114 |
115 | it('EC2NodeClass should accept valid name and valid properties', () => {
116 | const app = new cdk.App();
117 | const stack = new cdk.Stack(app, 'test-stack');
118 |
119 | const cluster = new Cluster(stack, 'testcluster', {
120 | version: KubernetesVersion.V1_27,
121 | });
122 |
123 | const karpenter = new Karpenter(stack, 'Karpenter', {
124 | cluster: cluster,
125 | version: 'v0.32.0',
126 | });
127 |
128 | expect(
129 | () => karpenter.addEC2NodeClass('ec2nodeclassvalid', {
130 | amiFamily: 'AL2',
131 | subnetSelectorTerms: [
132 | {
133 | tags: {
134 | Name: 'something',
135 | },
136 | },
137 | ],
138 | securityGroupSelectorTerms: [
139 | {
140 | tags: {
141 | 'aws:eks:cluster-name': cluster.clusterName,
142 | },
143 | },
144 | ],
145 | role: karpenter.nodeRole.roleName,
146 | }),
147 | ).not.toThrowError();
148 | });
149 |
150 | it('EC2NodeClass should fail on invalid properties', () => {
151 | const app = new cdk.App();
152 | const stack = new cdk.Stack(app, 'test-stack');
153 |
154 | const cluster = new Cluster(stack, 'testcluster', {
155 | version: KubernetesVersion.V1_27,
156 | });
157 |
158 | const karpenter = new Karpenter(stack, 'Karpenter', {
159 | cluster: cluster,
160 | version: 'v0.32.0',
161 | });
162 |
163 | expect(
164 | () => karpenter.addEC2NodeClass('ec2nodeclassvalid', {
165 | // amiFamily missing here
166 | subnetSelectorTerms: [
167 | {
168 | tags: {
169 | Name: 'something',
170 | },
171 | },
172 | ],
173 | securityGroupSelectorTerms: [
174 | {
175 | tags: {
176 | 'aws:eks:cluster-name': cluster.clusterName,
177 | },
178 | },
179 | ],
180 | role: karpenter.nodeRole.roleName,
181 | }),
182 | ).toThrowError();
183 | });
184 |
185 | it('NodePool should fail on invalid name and correct properties', () => {
186 | const app = new cdk.App();
187 | const stack = new cdk.Stack(app, 'test-stack');
188 |
189 | const cluster = new Cluster(stack, 'testcluster', {
190 | version: KubernetesVersion.V1_27,
191 | });
192 |
193 | const karpenter = new Karpenter(stack, 'Karpenter', {
194 | cluster: cluster,
195 | version: 'v0.32.0',
196 | });
197 |
198 | expect(
199 | () => karpenter.addNodePool('NodEPoooolInvalid', {
200 | template: {
201 | spec: {
202 | nodeClassRef: {
203 | apiVersion: 'karpenter.k8s.aws/v1beta1',
204 | kind: 'EC2NodeClass',
205 | name: 'nodeclassname',
206 | },
207 | requirements: [
208 | {
209 | key: 'karpenter.k8s.aws/instance-category',
210 | operator: 'In',
211 | values: ['m'],
212 | },
213 | ],
214 | },
215 | },
216 | }),
217 | ).toThrowError();
218 | });
219 |
220 | it('NodePool should accept correct name and properties', () => {
221 | const app = new cdk.App();
222 | const stack = new cdk.Stack(app, 'test-stack');
223 |
224 | const cluster = new Cluster(stack, 'testcluster', {
225 | version: KubernetesVersion.V1_27,
226 | });
227 |
228 | const karpenter = new Karpenter(stack, 'Karpenter', {
229 | cluster: cluster,
230 | version: 'v0.32.0',
231 | });
232 |
233 | expect(
234 | () => karpenter.addNodePool('nodepoolname', {
235 | template: {
236 | spec: {
237 | nodeClassRef: {
238 | apiVersion: 'karpenter.k8s.aws/v1beta1',
239 | kind: 'EC2NodeClass',
240 | name: 'nodeclassname',
241 | },
242 | requirements: [
243 | {
244 | key: 'karpenter.k8s.aws/instance-category',
245 | operator: 'In',
246 | values: ['m'],
247 | },
248 | ],
249 | },
250 | },
251 | }),
252 | ).not.toThrowError();
253 | });
254 |
255 | it('NodePool should fail on valid name but invalid properties', () => {
256 | const app = new cdk.App();
257 | const stack = new cdk.Stack(app, 'test-stack');
258 |
259 | const cluster = new Cluster(stack, 'testcluster', {
260 | version: KubernetesVersion.V1_27,
261 | });
262 |
263 | const karpenter = new Karpenter(stack, 'Karpenter', {
264 | cluster: cluster,
265 | version: 'v0.32.0',
266 | });
267 |
268 | expect(
269 | () => karpenter.addNodePool('nodepoolname', {
270 | template: {
271 | spec: {
272 | nodeClassRef: {
273 | apiVersion: 'karpenter.k8s.aws/v1beta1',
274 | kind: 'EC2NodeClass',
275 | name: 'nodeclassname',
276 | },
277 | // requirements key missing here
278 | },
279 | },
280 | }),
281 | ).toThrowError();
282 | });
283 | });
284 |
--------------------------------------------------------------------------------
/test/integ.karpenter.ts:
--------------------------------------------------------------------------------
1 | import { KubectlV31Layer } from '@aws-cdk/lambda-layer-kubectl-v31';
2 | import { App, CfnOutput, Stack, StackProps } from 'aws-cdk-lib';
3 | import { Vpc } from 'aws-cdk-lib/aws-ec2';
4 | import { AuthenticationMode, Cluster, CoreDnsComputeType, KubernetesVersion } from 'aws-cdk-lib/aws-eks';
5 | import { ManagedPolicy, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
6 | import { Construct } from 'constructs';
7 |
8 | import { Karpenter } from '../src';
9 |
10 | class TestEKSStack extends Stack {
11 | constructor(scope: Construct, id: string, props: StackProps = {}) {
12 | super(scope, id, props);
13 |
14 | const vpc = new Vpc(this, 'testVPC', {
15 | natGateways: 1,
16 | });
17 |
18 | const clusterRole = new Role(this, 'clusterRole', {
19 | assumedBy: new ServicePrincipal('eks.amazonaws.com'),
20 | managedPolicies: [
21 | ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSClusterPolicy'),
22 | ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSVPCResourceController'),
23 | ],
24 | });
25 |
26 | const cluster = new Cluster(this, 'testCluster', {
27 | vpc: vpc,
28 | role: clusterRole,
29 | version: KubernetesVersion.V1_31, // OCI HELM repo only supported by new version.
30 | defaultCapacity: 0,
31 | coreDnsComputeType: CoreDnsComputeType.FARGATE,
32 | kubectlLayer: new KubectlV31Layer(this, 'KubectlLayer'), // new Kubectl lambda layer
33 | authenticationMode: AuthenticationMode.API_AND_CONFIG_MAP,
34 | });
35 |
36 | cluster.addFargateProfile('karpenter', {
37 | selectors: [
38 | {
39 | namespace: 'karpenter',
40 | },
41 | {
42 | namespace: 'kube-system',
43 | labels: {
44 | 'k8s-app': 'kube-dns',
45 | },
46 | },
47 | ],
48 | });
49 |
50 | const karpenter = new Karpenter(this, 'Karpenter', {
51 | cluster: cluster,
52 | version: '1.1.1', // test a recent version
53 | });
54 |
55 | const nodeClass = karpenter.addEC2NodeClass('nodeclass', {
56 | amiFamily: 'Bottlerocket',
57 | amiSelectorTerms: [
58 | {
59 | alias: 'bottlerocket@latest',
60 | },
61 | ],
62 | subnetSelectorTerms: [
63 | {
64 | tags: {
65 | Name: `${this.stackName}/${vpc.node.id}/PrivateSubnet*`,
66 | },
67 | },
68 | ],
69 | securityGroupSelectorTerms: [
70 | {
71 | tags: {
72 | 'aws:eks:cluster-name': cluster.clusterName,
73 | },
74 | },
75 | ],
76 | role: karpenter.nodeRole.roleName,
77 | });
78 |
79 | karpenter.addNodePool('nodepool', {
80 | template: {
81 | spec: {
82 | nodeClassRef: {
83 | group: 'karpenter.k8s.aws',
84 | kind: 'EC2NodeClass',
85 | name: nodeClass.name,
86 | },
87 | requirements: [
88 | {
89 | key: 'karpenter.k8s.aws/instance-category',
90 | operator: 'In',
91 | values: ['m'],
92 | },
93 | {
94 | key: 'kubernetes.io/arch',
95 | operator: 'In',
96 | values: ['amd64'],
97 | },
98 | ],
99 | },
100 | },
101 | });
102 |
103 | karpenter.addManagedPolicyToKarpenterRole(ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'));
104 |
105 | new CfnOutput(this, 'ClusterName', {
106 | value: cluster.clusterName,
107 | });
108 |
109 | new CfnOutput(this, 'ClusterAdminRole', {
110 | value: cluster.adminRole.roleArn,
111 | });
112 |
113 | new CfnOutput(this, 'UpdateKubeConfigCommand', {
114 | value: `aws eks update-kubeconfig --name ${cluster.clusterName} --role-arn ${cluster.adminRole.roleArn}`,
115 | });
116 | }
117 | }
118 |
119 | const app = new App();
120 |
121 | new TestEKSStack(app, 'test', {
122 | env: {
123 | account: process.env.CDK_DEFAULT_ACCOUNT,
124 | region: process.env.CDK_DEFAULT_REGION,
125 | },
126 | });
127 |
128 | app.synth();
129 |
--------------------------------------------------------------------------------
/test/roles.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Match, Template } from 'aws-cdk-lib/assertions';
3 | import { Cluster, KubernetesVersion } from 'aws-cdk-lib/aws-eks';
4 | import { Role, ServicePrincipal, PolicyDocument, ManagedPolicy } from 'aws-cdk-lib/aws-iam';
5 | import { Karpenter } from '../src';
6 |
7 | describe('Karpenter installation', () => {
8 | it('should use existing nodeRole instead of creating a new role', () => {
9 | const app = new cdk.App();
10 | const stack = new cdk.Stack(app, 'test-stack');
11 |
12 | const cluster = new Cluster(stack, 'testcluster', {
13 | version: KubernetesVersion.V1_27,
14 | });
15 |
16 | const preexistingRole = new Role(stack, 'PreExistingRole', {
17 | assumedBy: new ServicePrincipal('lambda.amazonaws.com'),
18 | description: 'Example role...',
19 | });
20 |
21 | // Create Karpenter install with non-default version
22 | new Karpenter(stack, 'Karpenter', {
23 | cluster: cluster,
24 | nodeRole: preexistingRole,
25 | version: 'v0.32.0',
26 | });
27 |
28 | const t = Template.fromStack(stack);
29 | t.hasResource('Custom::AWSCDK-EKS-Cluster', {});
30 | t.resourceCountIs('AWS::IAM::Role', 8);
31 | t.hasResourceProperties('AWS::IAM::InstanceProfile', {
32 | Roles: Match.arrayWith( [
33 | Match.objectLike({
34 | Ref: Match.stringLikeRegexp('^.*PreExistingRole.*$'),
35 | }),
36 | ]),
37 | });
38 | });
39 |
40 | it('should be able to add managed policies to Karpenter Role', () => {
41 | const app = new cdk.App();
42 | const stack = new cdk.Stack(app, 'test-stack');
43 |
44 | const cluster = new Cluster(stack, 'testcluster', {
45 | version: KubernetesVersion.V1_27,
46 | });
47 |
48 | // Create Karpenter install with non-default version
49 | const karpenter = new Karpenter(stack, 'Karpenter', {
50 | cluster: cluster,
51 | version: 'v0.32.0',
52 | });
53 |
54 | const policyDocument = {
55 | Version: '2012-10-17',
56 | Statement: [
57 | {
58 | Sid: 'Statement',
59 | Effect: 'Allow',
60 | Action: 's3:ListAllMyBuckets',
61 | Resource: '*',
62 | },
63 | ],
64 | };
65 |
66 | const customPolicyDocument = PolicyDocument.fromJson(policyDocument);
67 |
68 | const newManagedPolicy = new ManagedPolicy(stack, 'MyNewManagedPolicy', {
69 | document: customPolicyDocument,
70 | });
71 |
72 | karpenter.addManagedPolicyToKarpenterRole(newManagedPolicy);
73 |
74 | karpenter.addManagedPolicyToKarpenterRole(ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore'));
75 |
76 | const t = Template.fromStack(stack);
77 | // Test if we have created the managed policy correctly.
78 | t.hasResource('AWS::IAM::ManagedPolicy', {
79 | Properties: {
80 | PolicyDocument: policyDocument,
81 | Description: '',
82 | Path: '/',
83 | },
84 | });
85 |
86 | // Test if the managed policy is attached to the Karpenter role
87 | t.hasResourceProperties('AWS::IAM::Role', Match.objectLike({
88 | ManagedPolicyArns: Match.arrayWith([
89 | Match.objectEquals({
90 | Ref: Match.stringLikeRegexp('^MyNewManagedPolicy.*$'),
91 | }),
92 | ]),
93 | }));
94 | });
95 | });
--------------------------------------------------------------------------------
/test/serviceaccount.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Template, Capture } from 'aws-cdk-lib/assertions';
3 | import { Cluster, KubernetesVersion } from 'aws-cdk-lib/aws-eks';
4 | import { Karpenter } from '../src';
5 |
6 | describe('Kubernetes ServiceAccount', () => {
7 | it('should create SA named: karpenter (default)', () => {
8 | const app = new cdk.App();
9 | const stack = new cdk.Stack(app, 'test-stack');
10 |
11 | const cluster = new Cluster(stack, 'testcluster', {
12 | version: KubernetesVersion.V1_27,
13 | });
14 |
15 | new Karpenter(stack, 'Karpenter', {
16 | cluster: cluster,
17 | version: 'v0.32.0',
18 | });
19 |
20 | const t = Template.fromStack(stack);
21 | // Test if we have created a ServiceAccount
22 | const valueCapture = new Capture();
23 | t.hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', {
24 | Values: valueCapture,
25 | });
26 |
27 | const values = valueCapture.asObject();
28 | expect(values['Fn::Join'][1][0]).toContain('{\"serviceAccount\":{\"create\":false,\"name\":\"karpenter\"');
29 | });
30 |
31 | it('should create SA named: custom-sa', () => {
32 | const app = new cdk.App();
33 | const stack = new cdk.Stack(app, 'test-stack');
34 |
35 | const cluster = new Cluster(stack, 'testcluster', {
36 | version: KubernetesVersion.V1_27,
37 | });
38 |
39 | new Karpenter(stack, 'Karpenter', {
40 | cluster: cluster,
41 | serviceAccountName: 'custom-sa',
42 | version: 'v0.32.0',
43 | });
44 |
45 | const t = Template.fromStack(stack);
46 | // Test if we have created a ServiceAccount
47 | const valueCapture = new Capture();
48 | t.hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', {
49 | Values: valueCapture,
50 | });
51 |
52 | const values = valueCapture.asObject();
53 | expect(values['Fn::Join'][1][0]).toContain('{\"serviceAccount\":{\"create\":false,\"name\":\"custom-sa\"');
54 | });
55 | });
56 |
--------------------------------------------------------------------------------
/test/utils.test.ts:
--------------------------------------------------------------------------------
1 | import { Utils } from '../src/utils';
2 |
3 | describe('Kubernetes name validation', () => {
4 | it('should return true for valid names', () => {
5 | expect(
6 | Utils.validateKubernetesNameConformance('my-valid-name'),
7 | ).toBeTruthy();
8 | });
9 |
10 | it('should return false for invalid names', () => {
11 | expect(
12 | Utils.validateKubernetesNameConformance('myInvalidName'),
13 | ).toBeFalsy();
14 | });
15 | });
16 |
17 | describe('Object validation', () => {
18 | it('should not throw error if all required keys are present', () => {
19 | expect(
20 | () => Utils.hasRequiredKeys({
21 | requirements: {},
22 | nodeRef: {},
23 | someRequiredString: 'mystring',
24 | someOtherThingWhichIsNotRequired: ['a', 'b', 'c'],
25 | }, ['requirements', 'nodeRef', 'someRequiredString']),
26 | ).not.toThrowError();
27 | });
28 |
29 | it('should throw error is required keys are missing', () => {
30 | expect(
31 | () => Utils.hasRequiredKeys({
32 | someOtherThingWhichIsNotRequired: ['a', 'b', 'c'],
33 | }, ['requirements', 'nodeRef']),
34 | ).toThrowError();
35 | });
36 | });
--------------------------------------------------------------------------------
/test/versions.test.ts:
--------------------------------------------------------------------------------
1 | import * as cdk from 'aws-cdk-lib';
2 | import { Match, Template } from 'aws-cdk-lib/assertions';
3 | import { Cluster, KubernetesVersion } from 'aws-cdk-lib/aws-eks';
4 | import { Karpenter } from '../src';
5 |
6 | describe('Karpenter Versions', () => {
7 | it('should install from old URL if Karpenter version < v0.17.0', () => {
8 | const app = new cdk.App();
9 | const stack = new cdk.Stack(app, 'test-stack');
10 |
11 | const cluster = new Cluster(stack, 'testcluster', {
12 | version: KubernetesVersion.V1_27,
13 | });
14 |
15 | // Create Karpenter install with non-default version
16 | new Karpenter(stack, 'Karpenter', {
17 | cluster: cluster,
18 | version: 'v0.6.0',
19 | });
20 |
21 | const t = Template.fromStack(stack);
22 | t.hasResource('Custom::AWSCDK-EKS-Cluster', {});
23 | t.hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', {
24 | Repository: Match.stringLikeRegexp('https://charts.karpenter.sh'),
25 | });
26 | });
27 |
28 | it('should install from new URL if Karpenter version >= v0.17.0', () => {
29 | const app = new cdk.App();
30 | const stack = new cdk.Stack(app, 'test-stack');
31 |
32 | const cluster = new Cluster(stack, 'testcluster', {
33 | version: KubernetesVersion.V1_27,
34 | });
35 |
36 | // Create Karpenter install with non-default version
37 | new Karpenter(stack, 'Karpenter', {
38 | cluster: cluster,
39 | version: 'v0.17.0',
40 | });
41 |
42 | const t = Template.fromStack(stack);
43 | t.hasResource('Custom::AWSCDK-EKS-Cluster', {});
44 | t.hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', {
45 | Repository: Match.stringLikeRegexp('oci://public.ecr.aws/karpenter/karpenter'),
46 | });
47 | });
48 |
49 | it('should use helm settings for Karpenter between version v0.19.0 and v0.32.0', () => {
50 | const app = new cdk.App();
51 | const stack = new cdk.Stack(app, 'test-stack');
52 |
53 | const cluster = new Cluster(stack, 'testcluster', {
54 | version: KubernetesVersion.V1_27,
55 | });
56 |
57 | // Create Karpenter install with non-default version
58 | const karpenter = new Karpenter(stack, 'Karpenter', {
59 | cluster: cluster,
60 | version: 'v0.19.1',
61 | });
62 |
63 | const t = Template.fromStack(stack);
64 | t.hasResource('Custom::AWSCDK-EKS-Cluster', {});
65 | t.hasResource('Custom::AWSCDK-EKS-HelmChart', {});
66 |
67 | expect(karpenter.helmChartValues).toEqual(
68 | expect.objectContaining({
69 | settings: expect.objectContaining({
70 | aws: expect.objectContaining({
71 | clusterName: expect.any(String),
72 | clusterEndpoint: expect.any(String),
73 | }),
74 | }),
75 | }),
76 | );
77 | });
78 |
79 | it('should throw an exception for Karpenter >=v0.32.0 and addNodeTemplate()', () => {
80 | const app = new cdk.App();
81 | const stack = new cdk.Stack(app, 'test-stack');
82 |
83 | const cluster = new Cluster(stack, 'testcluster', {
84 | version: KubernetesVersion.V1_27,
85 | });
86 |
87 | const karpenter = new Karpenter(stack, 'Karpenter', {
88 | cluster: cluster,
89 | serviceAccountName: 'custom-sa',
90 | version: 'v0.32.0',
91 | });
92 |
93 | expect(
94 | () => karpenter.addNodeTemplate('test', {}),
95 | ).toThrowError();
96 | });
97 |
98 | it('should throw an exception for Karpenter >=v0.32.0 and addProvisioner()', () => {
99 | const app = new cdk.App();
100 | const stack = new cdk.Stack(app, 'test-stack');
101 |
102 | const cluster = new Cluster(stack, 'testcluster', {
103 | version: KubernetesVersion.V1_27,
104 | });
105 |
106 | const karpenter = new Karpenter(stack, 'Karpenter', {
107 | cluster: cluster,
108 | serviceAccountName: 'custom-sa',
109 | version: 'v0.32.0',
110 | });
111 |
112 | expect(
113 | () => karpenter.addProvisioner('test', {}),
114 | ).toThrowError();
115 | });
116 |
117 | it('should allow for creation of v1 APIs', () => {
118 | const app = new cdk.App();
119 | const stack = new cdk.Stack(app, 'test-stack');
120 |
121 | const cluster = new Cluster(stack, 'testcluster', {
122 | version: KubernetesVersion.V1_27,
123 | });
124 |
125 | const karpenter = new Karpenter(stack, 'Karpenter', {
126 | cluster: cluster,
127 | serviceAccountName: 'custom-sa',
128 | version: 'v0.32.0',
129 | });
130 |
131 | const nodeClass = karpenter.addEC2NodeClass('ec2nodeclass', {
132 | amiFamily: 'AL2',
133 | subnetSelectorTerms: [],
134 | securityGroupSelectorTerms: [],
135 | role: karpenter.nodeRole.roleName,
136 | });
137 |
138 | karpenter.addNodePool('nodepool', {
139 | template: {
140 | spec: {
141 | nodeClassRef: {
142 | apiVersion: 'karpenter.k8s.aws/v1',
143 | kind: 'EC2NodeClass',
144 | name: nodeClass.name,
145 | },
146 | requirements: [
147 | {
148 | key: 'karpenter.k8s.aws/instance-category',
149 | operator: 'In',
150 | values: ['m'],
151 | },
152 | ],
153 | },
154 | },
155 | });
156 |
157 | const t = Template.fromStack(stack);
158 |
159 | // EC2NodeClass manifest
160 | t.hasResourceProperties('Custom::AWSCDK-EKS-KubernetesResource', Match.objectLike({
161 | Manifest: Match.objectLike({
162 | 'Fn::Join': [
163 | '',
164 | Match.arrayWith([
165 | Match.stringLikeRegexp('\"apiVersion\":\"karpenter.k8s.aws\/v1\",\"kind\":\"EC2NodeClass\",\"metadata\":{\"name\":\"ec2nodeclass\",\"namespace\":\"karpenter\"'),
166 | ]),
167 | ],
168 | }),
169 | }));
170 |
171 | // NodePool manifest
172 | t.hasResourceProperties('Custom::AWSCDK-EKS-KubernetesResource', Match.objectLike({
173 | Manifest: Match.stringLikeRegexp('\"apiVersion\":\"karpenter.sh\/v1\",\"kind\":\"NodePool\",\"metadata\":{\"name\":\"nodepool\",\"namespace\":\"karpenter\"'),
174 | }));
175 | });
176 |
177 | it('should use correct helm values for if >= v0.32.0', () => {
178 | const app = new cdk.App();
179 | const stack = new cdk.Stack(app, 'test-stack');
180 |
181 | const cluster = new Cluster(stack, 'testcluster', {
182 | version: KubernetesVersion.V1_27,
183 | });
184 |
185 | const karpenter = new Karpenter(stack, 'Karpenter', {
186 | cluster: cluster,
187 | version: 'v0.32.0',
188 | });
189 |
190 | expect(karpenter.helmChartValues).not.toContain(
191 | expect.objectContaining({
192 | aws: expect.anything,
193 | }),
194 | );
195 | expect(karpenter.helmChartValues).toEqual(
196 | expect.objectContaining({
197 | settings: expect.objectContaining({
198 | clusterName: expect.any(String),
199 | clusterEndpoint: expect.any(String),
200 | interruptionQueue: expect.any(String),
201 | }),
202 | }),
203 | );
204 | });
205 | });
206 |
--------------------------------------------------------------------------------
/tsconfig.dev.json:
--------------------------------------------------------------------------------
1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".
2 | {
3 | "compilerOptions": {
4 | "alwaysStrict": true,
5 | "declaration": true,
6 | "esModuleInterop": true,
7 | "experimentalDecorators": true,
8 | "inlineSourceMap": true,
9 | "inlineSources": true,
10 | "lib": [
11 | "es2019"
12 | ],
13 | "module": "CommonJS",
14 | "noEmitOnError": false,
15 | "noFallthroughCasesInSwitch": true,
16 | "noImplicitAny": true,
17 | "noImplicitReturns": true,
18 | "noImplicitThis": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "resolveJsonModule": true,
22 | "strict": true,
23 | "strictNullChecks": true,
24 | "strictPropertyInitialization": true,
25 | "stripInternal": true,
26 | "target": "ES2019"
27 | },
28 | "include": [
29 | ".projenrc.js",
30 | "src/**/*.ts",
31 | "test/**/*.ts"
32 | ],
33 | "exclude": [
34 | "node_modules"
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------