├── .github ├── FUNDING.yml └── workflows │ └── CI.yml ├── .gitignore ├── LICENSE ├── README.md ├── argparser ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── lib │ ├── index.d.ts │ └── index.js ├── package.json ├── test │ └── index.js └── tsconfig.json ├── cli ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── lib │ ├── commands │ │ ├── add.d.ts │ │ ├── add.js │ │ ├── add_api.d.ts │ │ ├── add_api.js │ │ ├── add_webhook.d.ts │ │ ├── add_webhook.js │ │ ├── codegen.d.ts │ │ ├── codegen.js │ │ ├── configure.d.ts │ │ ├── configure.js │ │ ├── configure_manager_image.d.ts │ │ ├── configure_manager_image.js │ │ ├── init.d.ts │ │ └── init.js │ ├── index.d.ts │ ├── index.js │ ├── project.d.ts │ ├── project.js │ ├── rbac.d.ts │ ├── rbac.js │ └── templates │ │ ├── add_api.d.ts │ │ ├── add_api.js │ │ ├── add_webhook.d.ts │ │ ├── add_webhook.js │ │ ├── init.d.ts │ │ └── init.js ├── package.json └── tsconfig.json ├── controller-runtime ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── lib │ ├── apimachinery │ │ ├── errors.d.ts │ │ ├── errors.js │ │ ├── meta │ │ │ ├── v1.d.ts │ │ │ └── v1.js │ │ ├── schema.d.ts │ │ ├── schema.js │ │ ├── types.d.ts │ │ └── types.js │ ├── builder.d.ts │ ├── builder.js │ ├── client.d.ts │ ├── client.js │ ├── context.d.ts │ ├── context.js │ ├── controller.d.ts │ ├── controller.js │ ├── controllerutil.d.ts │ ├── controllerutil.js │ ├── index.d.ts │ ├── index.js │ ├── leaderelection │ │ ├── leaderelection.d.ts │ │ ├── leaderelection.js │ │ ├── leaselock.d.ts │ │ └── leaselock.js │ ├── manager.d.ts │ ├── manager.js │ ├── queue.d.ts │ ├── queue.js │ ├── reconcile.d.ts │ ├── reconcile.js │ ├── record │ │ ├── recorder.d.ts │ │ └── recorder.js │ ├── source.d.ts │ ├── source.js │ ├── util.d.ts │ ├── util.js │ └── webhook │ │ ├── admission.d.ts │ │ ├── admission.js │ │ ├── server.d.ts │ │ └── server.js ├── package.json ├── scripts │ └── clean-types.js ├── test │ ├── apimachinery │ │ ├── errors.js │ │ └── schema.js │ ├── builder.js │ ├── context.js │ ├── controller.js │ ├── controllerutil.js │ ├── interop.mjs │ ├── leaderelection │ │ ├── leaderelection.js │ │ ├── leaselock.js │ │ └── test-utils.js │ ├── manager.js │ ├── reconcile.js │ ├── record │ │ └── recorder.js │ ├── source.js │ ├── test-utils.js │ └── webhook │ │ ├── admission.js │ │ └── server.js └── tsconfig.json ├── crdgen ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── fixtures │ ├── book.ts │ ├── functions.ts │ ├── supported-types.ts │ └── unsupported-types.ts ├── lib │ ├── index.d.ts │ ├── index.js │ ├── model.d.ts │ └── model.js ├── package.json ├── test │ └── typescript.js └── tsconfig.json ├── extension-server ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── lib │ ├── server.d.ts │ └── server.js ├── package.json ├── test │ └── server.js └── tsconfig.json ├── kubenode ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── kubenode.js └── package.json └── reference ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── lib ├── index.d.ts └── index.js ├── package.json ├── test └── index.js └── tsconfig.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cjihrig] 4 | patreon: cjihrig 5 | custom: https://www.paypal.me/cjihrig/100 6 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: kubenode CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | node-version: [20.x, 22.x, 24.x] 17 | os: [ubuntu-latest, macos-latest, windows-latest] 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | 26 | - name: argparser CI 27 | run: cd argparser && npm install && npm test 28 | env: 29 | CI: true 30 | - name: cli CI 31 | run: cd cli && npm install && npm test 32 | env: 33 | CI: true 34 | - name: controller-runtime CI 35 | run: cd controller-runtime && npm install && npm test 36 | env: 37 | CI: true 38 | - name: crdgen CI 39 | run: cd crdgen && npm install && npm test 40 | env: 41 | CI: true 42 | - name: extension-server CI 43 | run: cd extension-server && npm install && npm test 44 | env: 45 | CI: true 46 | - name: reference CI 47 | run: cd reference && npm install && npm test 48 | env: 49 | CI: true 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Colin Ihrig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /argparser/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /argparser/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | -------------------------------------------------------------------------------- /argparser/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Colin Ihrig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /argparser/README.md: -------------------------------------------------------------------------------- 1 | # `@kubenode/argparser` 2 | 3 | Generic package for building Command Line Interfaces (CLIs). This package is 4 | built on top of `util.parseArgs()`. It supports command hierarchy and 5 | automatically generated help/usage. 6 | -------------------------------------------------------------------------------- /argparser/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} CommandFlag 3 | * @property {string} type The data type of the flag. 4 | * @property {boolean} multiple Indicates whether or not the flag may be specified multiple times. 5 | * @property {string} [short] The short name of the flag. 6 | * @property {any} [default] The default value of the flag. 7 | * @property {string} [description] A textual description of the flag. 8 | * 9 | * @typedef {Object} Command 10 | * @property {string} name The name of the command. 11 | * @property {string} [description] A textual description of the command. 12 | * @property {any} [subcommands] Subcommands of the current command. 13 | * @property {function} [run] Executable functionality of the command. 14 | * @property {Object} [globalFlags] Flags available to this command and all subcommands. 15 | * @property {Object} [flags] Flags available to this command. 16 | * @property {Object} [parserOptions] Additional options to pass to util.parseArgs(). 17 | */ 18 | /** 19 | * Parses input arguments given a command structure. 20 | * @param {Command} root The root command to parse. 21 | * @param {string[]} args The arguments to apply to the command. 22 | */ 23 | export function parse(root: Command, args: string[]): { 24 | command: Command; 25 | flags: { 26 | [longOption: string]: string | boolean | (string | boolean)[]; 27 | }; 28 | positionals: string[]; 29 | }; 30 | declare namespace _default { 31 | export { parse }; 32 | } 33 | export default _default; 34 | export type CommandFlag = { 35 | /** 36 | * The data type of the flag. 37 | */ 38 | type: string; 39 | /** 40 | * Indicates whether or not the flag may be specified multiple times. 41 | */ 42 | multiple: boolean; 43 | /** 44 | * The short name of the flag. 45 | */ 46 | short?: string; 47 | /** 48 | * The default value of the flag. 49 | */ 50 | default?: any; 51 | /** 52 | * A textual description of the flag. 53 | */ 54 | description?: string; 55 | }; 56 | export type Command = { 57 | /** 58 | * The name of the command. 59 | */ 60 | name: string; 61 | /** 62 | * A textual description of the command. 63 | */ 64 | description?: string; 65 | /** 66 | * Subcommands of the current command. 67 | */ 68 | subcommands?: any; 69 | /** 70 | * Executable functionality of the command. 71 | */ 72 | run?: Function; 73 | /** 74 | * Flags available to this command and all subcommands. 75 | */ 76 | globalFlags?: any; 77 | /** 78 | * Flags available to this command. 79 | */ 80 | flags?: any; 81 | /** 82 | * Additional options to pass to util.parseArgs(). 83 | */ 84 | parserOptions?: any; 85 | }; 86 | -------------------------------------------------------------------------------- /argparser/lib/index.js: -------------------------------------------------------------------------------- 1 | import { parseArgs } from 'node:util'; 2 | 3 | /** 4 | * @typedef {Object} CommandFlag 5 | * @property {string} type The data type of the flag. 6 | * @property {boolean} multiple Indicates whether or not the flag may be specified multiple times. 7 | * @property {string} [short] The short name of the flag. 8 | * @property {any} [default] The default value of the flag. 9 | * @property {string} [description] A textual description of the flag. 10 | * 11 | * @typedef {Object} Command 12 | * @property {string} name The name of the command. 13 | * @property {string} [description] A textual description of the command. 14 | * @property {any} [subcommands] Subcommands of the current command. 15 | * @property {function} [run] Executable functionality of the command. 16 | * @property {Object} [globalFlags] Flags available to this command and all subcommands. 17 | * @property {Object} [flags] Flags available to this command. 18 | * @property {Object} [parserOptions] Additional options to pass to util.parseArgs(). 19 | */ 20 | 21 | /** 22 | * Parses input arguments given a command structure. 23 | * @param {Command} root The root command to parse. 24 | * @param {string[]} args The arguments to apply to the command. 25 | */ 26 | export function parse(root, args) { 27 | let command = root; 28 | let globalFlags = { ...root.globalFlags }; 29 | const consumed = []; 30 | 31 | const { 32 | positionals: rootPositionals = [] 33 | } = runParseArgs(args, command, globalFlags); 34 | 35 | for (let i = 0; i < rootPositionals.length; ++i) { 36 | const subcommands = command.subcommands?.(); 37 | if (subcommands === undefined) { 38 | break; 39 | } 40 | 41 | const positional = rootPositionals[i]; 42 | const cmd = subcommands.get(positional); 43 | if (cmd === undefined) { 44 | break; 45 | } 46 | 47 | if (cmd.globalFlags) { 48 | globalFlags = { ...globalFlags, ...cmd.globalFlags }; 49 | } 50 | 51 | consumed.push(positional); 52 | command = cmd; 53 | } 54 | 55 | if (typeof command.run !== 'function') { 56 | usage(command, root, globalFlags, consumed); 57 | } 58 | 59 | const { 60 | values: flags, 61 | positionals 62 | } = runParseArgs(args, command, globalFlags); 63 | 64 | return { command, flags, positionals }; 65 | } 66 | 67 | /** 68 | * Run Node's util.parseArgs(). 69 | * @param {string[]} args The arguments to apply to the command. 70 | * @param {Command} command The command to apply the arguments to. 71 | * @param {Object} globalFlags The current global flags. 72 | */ 73 | function runParseArgs(args, command, globalFlags) { 74 | const config = { 75 | allowPositionals: true, 76 | strict: false, 77 | ...command.parserOptions, 78 | options: { 79 | ...globalFlags, 80 | ...command.flags 81 | }, 82 | args 83 | }; 84 | 85 | return parseArgs(config); 86 | } 87 | 88 | /** 89 | * Throws an exception containing usage text. 90 | * @param {Command} command The command to generate usage text for. 91 | * @param {Command} root The root command. 92 | * @param {Object} globalFlags The current global flags. 93 | * @param {string[]} consumedPositionals The arguments that have been consumed by the parser. 94 | */ 95 | function usage(command, root, globalFlags, consumedPositionals) { 96 | /** @type {Map|undefined} */ 97 | const subcommands = command.subcommands?.(); 98 | let usage = ''; 99 | 100 | usage += 'Usage:\n'; 101 | usage += ` ${root.name}`; 102 | 103 | if (consumedPositionals.length > 0) { 104 | usage += ` ${consumedPositionals.join(' ')}`; 105 | } 106 | 107 | if (subcommands instanceof Map) { 108 | usage += ' [command]'; 109 | } 110 | 111 | usage += '\n'; 112 | 113 | if (subcommands instanceof Map) { 114 | let maxCmdLen = 0; 115 | 116 | for (const cmd of subcommands.values()) { 117 | if (cmd.name.length > maxCmdLen) { 118 | maxCmdLen = cmd.name.length; 119 | } 120 | } 121 | 122 | usage += '\nAvailable Commands:\n'; 123 | 124 | for (const cmd of subcommands.values()) { 125 | const padding = maxCmdLen - cmd.name.length; 126 | 127 | usage += ` ${cmd.name} ${' '.repeat(padding)} ${cmd.description}\n`; 128 | } 129 | } 130 | 131 | const combinedFlags = { ...globalFlags, ...command.flags }; 132 | const flagEntries = Object.entries(combinedFlags); 133 | if (flagEntries.length > 0) { 134 | let maxFlagLen = 0; 135 | let hasShort = false; 136 | 137 | for (const [k, v] of flagEntries) { 138 | const len = v.type === 'string' ? k.length + 7 : k.length; 139 | if (len > maxFlagLen) { 140 | maxFlagLen = len; 141 | } 142 | 143 | if (v.short) { 144 | hasShort = true; 145 | } 146 | } 147 | 148 | usage += '\nFlags:\n'; 149 | 150 | for (const [k, v] of flagEntries) { 151 | const short = v.short ? `-${v.short}, ` : hasShort ? ' ' : ''; 152 | const type = v.type === 'string' ? ' string' : ''; 153 | const flag = `${k}${type}`; 154 | const desc = v.description ? 155 | ` ${' '.repeat(maxFlagLen - flag.length)}${v.description}` : ''; 156 | usage += ` ${short}--${flag}${desc}\n`; 157 | } 158 | } 159 | 160 | throw new Error(usage); 161 | } 162 | 163 | export default { 164 | parse 165 | }; 166 | -------------------------------------------------------------------------------- /argparser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kubenode/argparser", 3 | "version": "0.1.1", 4 | "description": "Kubenode CLI argument parser", 5 | "license": "MIT", 6 | "author": "Colin J. Ihrig (http://www.cjihrig.com/)", 7 | "main": "lib/index.js", 8 | "type": "module", 9 | "scripts": { 10 | "//": "The linter is not happy about the move to ESM so it has been removed from the pretest script.", 11 | "lint": "belly-button -f", 12 | "pretest": "tsc --noEmit", 13 | "test": "node --test --experimental-test-coverage", 14 | "types": "rm lib/*.d.ts && tsc" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^22.7.5", 18 | "belly-button": "^8.0.0", 19 | "typescript": "^5.6.2" 20 | }, 21 | "homepage": "https://github.com/cjihrig/kubenode#readme", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/cjihrig/kubenode.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/cjihrig/kubenode/issues" 28 | }, 29 | "keywords": [ 30 | "kubernetes", 31 | "k8s", 32 | "argparser", 33 | "argument", 34 | "parser", 35 | "cli" 36 | ], 37 | "directories": { 38 | "lib": "lib", 39 | "test": "test" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /argparser/test/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { test } from 'node:test'; 3 | import { parse } from '../lib/index.js'; 4 | 5 | test('parses a simple command successfully', (t) => { 6 | const spy = t.mock.fn(); 7 | const root = { 8 | name: 'foo', 9 | run: spy, 10 | }; 11 | const result = parse(root, ['bar', '--baz', 5]); 12 | assert.deepStrictEqual(result, { 13 | command: root, 14 | flags: { __proto__: null, baz: true }, 15 | positionals: ['bar', 5], 16 | }); 17 | assert.strictEqual(spy.mock.calls.length, 0); 18 | assert.strictEqual(result.command.run(), undefined); 19 | assert.strictEqual(spy.mock.calls.length, 1); 20 | const call = spy.mock.calls[0]; 21 | assert.deepStrictEqual(call.arguments, []); 22 | assert.strictEqual(call.error, undefined); 23 | assert.strictEqual(call.result, undefined); 24 | assert.strictEqual(call.this, root); 25 | }); 26 | 27 | test('throws usage if resolved command is not runnable', () => { 28 | const root = { name: 'foo' }; 29 | assert.throws(() => { 30 | parse(root); 31 | }, (err) => { 32 | assert.match(err.message, /Usage:/); 33 | assert.match(err.message, /foo/); 34 | return true; 35 | }); 36 | }); 37 | 38 | test('parses a command with subcommands successfully', (t) => { 39 | const spy1 = t.mock.fn(); 40 | const spy2 = t.mock.fn(); 41 | const sub1 = { name: 'baz', run: spy1 }; 42 | const sub2 = { name: 'bar', run: spy2 }; 43 | const root = { 44 | name: 'foo', 45 | subcommands() { 46 | return new Map([ 47 | [sub1.name, sub1], 48 | [sub2.name, sub2], 49 | ]); 50 | } 51 | }; 52 | const result = parse(root, ['bar', '--baz', 5]); 53 | assert.deepStrictEqual(result, { 54 | command: sub2, 55 | flags: { __proto__: null, baz: true }, 56 | positionals: ['bar', 5], 57 | }); 58 | assert.strictEqual(spy2.mock.calls.length, 0); 59 | assert.strictEqual(result.command.run(), undefined); 60 | assert.strictEqual(spy2.mock.calls.length, 1); 61 | const call = spy2.mock.calls[0]; 62 | assert.deepStrictEqual(call.arguments, []); 63 | assert.strictEqual(call.error, undefined); 64 | assert.strictEqual(call.result, undefined); 65 | assert.strictEqual(call.this, sub2); 66 | assert.strictEqual(spy1.mock.calls.length, 0); 67 | }); 68 | -------------------------------------------------------------------------------- /argparser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "target": "es2022" 10 | }, 11 | "include": ["lib/**/*.js"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /cli/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | -------------------------------------------------------------------------------- /cli/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Colin Ihrig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cli/README.md: -------------------------------------------------------------------------------- 1 | # `@kubenode/cli` 2 | 3 | This package implements the commands of the Kubenode CLI. The documentation for 4 | each command can be found [here](https://github.com/cjihrig/kubenode?tab=readme-ov-file#cli-commands). 5 | -------------------------------------------------------------------------------- /cli/lib/commands/add.d.ts: -------------------------------------------------------------------------------- 1 | declare const kCommand: "add"; 2 | declare const kDescription: "Add resources to a project"; 3 | export function subcommands(): Map Promise; 27 | }>; 28 | export { kCommand as name, kDescription as description }; 29 | -------------------------------------------------------------------------------- /cli/lib/commands/add.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const kCommand = 'add'; 3 | const kDescription = 'Add resources to a project'; 4 | 5 | function subcommands() { 6 | const api = require('./add_api'); 7 | const webhook = require('./add_webhook'); 8 | // @ts-ignore 9 | const commands = new Map([ 10 | [api.name, api], 11 | [webhook.name, webhook] 12 | ]); 13 | 14 | return commands; 15 | } 16 | 17 | module.exports = { 18 | name: kCommand, 19 | description: kDescription, 20 | subcommands 21 | }; 22 | -------------------------------------------------------------------------------- /cli/lib/commands/add_api.d.ts: -------------------------------------------------------------------------------- 1 | declare const kCommand: "api"; 2 | declare const kDescription: "Add a new API to a project"; 3 | export namespace flags { 4 | namespace group { 5 | let type: string; 6 | let multiple: boolean; 7 | let short: string; 8 | let description: string; 9 | } 10 | namespace kind { 11 | let type_1: string; 12 | export { type_1 as type }; 13 | let multiple_1: boolean; 14 | export { multiple_1 as multiple }; 15 | let short_1: string; 16 | export { short_1 as short }; 17 | let description_1: string; 18 | export { description_1 as description }; 19 | } 20 | namespace version { 21 | let type_2: string; 22 | export { type_2 as type }; 23 | let multiple_2: boolean; 24 | export { multiple_2 as multiple }; 25 | let short_2: string; 26 | export { short_2 as short }; 27 | let description_2: string; 28 | export { description_2 as description }; 29 | } 30 | } 31 | export function run(flags: any, positionals: any): Promise; 32 | export { kCommand as name, kDescription as description }; 33 | -------------------------------------------------------------------------------- /cli/lib/commands/add_api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { mkdirSync, writeFileSync } = require('node:fs'); 3 | const { join, resolve } = require('node:path'); 4 | const pluralize = require('pluralize'); 5 | const { Project } = require('../project'); 6 | const { updateManagerRole } = require('../rbac'); 7 | const kCommand = 'api'; 8 | const kDescription = 'Add a new API to a project'; 9 | const flags = { 10 | group: { 11 | type: 'string', 12 | multiple: false, 13 | short: 'g', 14 | description: 'API group for the CRD.' 15 | }, 16 | kind: { 17 | type: 'string', 18 | multiple: false, 19 | short: 'k', 20 | description: 'Kind for the CRD.' 21 | }, 22 | version: { 23 | type: 'string', 24 | multiple: false, 25 | short: 'v', 26 | description: 'API version for the CRD.' 27 | } 28 | }; 29 | let templates; 30 | 31 | async function run(flags, positionals) { 32 | if (flags.group === undefined) { 33 | throw new Error('--group must be specified'); 34 | } 35 | 36 | if (flags.kind === undefined) { 37 | throw new Error('--kind must be specified'); 38 | } 39 | 40 | if (flags.version === undefined) { 41 | throw new Error('--version must be specified'); 42 | } 43 | 44 | const projectDir = resolve(flags.directory); 45 | const project = Project.fromDirectory(projectDir); 46 | const gvDirName = `${flags.group}_${flags.version}`.toLowerCase(); 47 | const kind = flags.kind.charAt(0).toUpperCase() + flags.kind.slice(1); 48 | const listKind = `${kind}List`; 49 | const singular = kind.toLowerCase(); 50 | const plural = pluralize(singular); 51 | const ctrlDir = join(projectDir, 'lib', 'controller', gvDirName); 52 | const ctrlFile = `${singular}.js`; 53 | const ctrl = join(ctrlDir, ctrlFile); 54 | const typePath = join(ctrlDir, `${singular}_types.ts`); 55 | const sampleDir = join(projectDir, 'config', 'samples'); 56 | const samplePath = join(sampleDir, `${gvDirName}_${singular}.yaml`); 57 | const rbacDir = join(projectDir, 'config', 'rbac'); 58 | const rbacConfig = join(rbacDir, 'manager_role.yaml'); 59 | const data = { 60 | group: flags.group, 61 | kind, 62 | listKind, 63 | plural, 64 | projectName: project.projectName, 65 | singular, 66 | version: flags.version 67 | }; 68 | const resource = project.ensureResource({ 69 | group: data.group, 70 | kind: data.kind, 71 | version: data.version 72 | }); 73 | 74 | if (resource.controller) { 75 | const gvk = `${resource.kind}.${resource.group}/${resource.version}`; 76 | throw new Error(`resource '${gvk}' already has a controller`); 77 | } 78 | 79 | resource.controller = true; 80 | lazyLoadTemplates(); 81 | mkdirSync(ctrlDir, { recursive: true }); 82 | mkdirSync(sampleDir, { recursive: true }); 83 | writeFileSync(ctrl, templates.controller(data)); 84 | writeFileSync(samplePath, templates.sample(data)); 85 | writeFileSync(typePath, templates.types(data)); 86 | updateManagerRole(rbacConfig, data); 87 | 88 | { 89 | // Update generated code in existing files. 90 | const srcDir = join(projectDir, 'lib'); 91 | const main = join(srcDir, 'index.js'); 92 | const ctrlImport = `import { ${kind}Reconciler } from ` + 93 | `'./controller/${gvDirName}/${ctrlFile}';`; 94 | const ctrlSetup = `(new ${kind}Reconciler()).setupWithManager(manager);`; 95 | project.inject(main, '@kubenode:scaffold:imports', ctrlImport); 96 | project.inject(main, '@kubenode:scaffold:manager', ctrlSetup); 97 | await project.writeMarkers(); 98 | } 99 | 100 | project.write(); 101 | } 102 | 103 | function lazyLoadTemplates() { 104 | templates = require('../templates/add_api'); 105 | } 106 | 107 | module.exports = { 108 | name: kCommand, 109 | description: kDescription, 110 | flags, 111 | run 112 | }; 113 | -------------------------------------------------------------------------------- /cli/lib/commands/add_webhook.d.ts: -------------------------------------------------------------------------------- 1 | declare const kCommand: "webhook"; 2 | declare const kDescription: "Add a new webhook to a project"; 3 | export namespace flags { 4 | namespace group { 5 | let type: string; 6 | let multiple: boolean; 7 | let short: string; 8 | let description: string; 9 | } 10 | namespace kind { 11 | let type_1: string; 12 | export { type_1 as type }; 13 | let multiple_1: boolean; 14 | export { multiple_1 as multiple }; 15 | let short_1: string; 16 | export { short_1 as short }; 17 | let description_1: string; 18 | export { description_1 as description }; 19 | } 20 | namespace mutating { 21 | let type_2: string; 22 | export { type_2 as type }; 23 | let _default: boolean; 24 | export { _default as default }; 25 | let multiple_2: boolean; 26 | export { multiple_2 as multiple }; 27 | let short_2: string; 28 | export { short_2 as short }; 29 | let description_2: string; 30 | export { description_2 as description }; 31 | } 32 | namespace validating { 33 | let type_3: string; 34 | export { type_3 as type }; 35 | let _default_1: boolean; 36 | export { _default_1 as default }; 37 | let multiple_3: boolean; 38 | export { multiple_3 as multiple }; 39 | let short_3: string; 40 | export { short_3 as short }; 41 | let description_3: string; 42 | export { description_3 as description }; 43 | } 44 | namespace version { 45 | let type_4: string; 46 | export { type_4 as type }; 47 | let multiple_4: boolean; 48 | export { multiple_4 as multiple }; 49 | let short_4: string; 50 | export { short_4 as short }; 51 | let description_4: string; 52 | export { description_4 as description }; 53 | } 54 | } 55 | export function run(flags: any, positionals: any): Promise; 56 | export { kCommand as name, kDescription as description }; 57 | -------------------------------------------------------------------------------- /cli/lib/commands/codegen.d.ts: -------------------------------------------------------------------------------- 1 | declare const kCommand: "codegen"; 2 | declare const kDescription: "Generate code from project resources"; 3 | export namespace flags { 4 | namespace group { 5 | let type: string; 6 | let multiple: boolean; 7 | let short: string; 8 | let description: string; 9 | } 10 | namespace kind { 11 | let type_1: string; 12 | export { type_1 as type }; 13 | let multiple_1: boolean; 14 | export { multiple_1 as multiple }; 15 | let short_1: string; 16 | export { short_1 as short }; 17 | let description_1: string; 18 | export { description_1 as description }; 19 | } 20 | namespace version { 21 | let type_2: string; 22 | export { type_2 as type }; 23 | let multiple_2: boolean; 24 | export { multiple_2 as multiple }; 25 | let short_2: string; 26 | export { short_2 as short }; 27 | let description_2: string; 28 | export { description_2 as description }; 29 | } 30 | } 31 | export function run(flags: any, positionals: any): Promise; 32 | export { kCommand as name, kDescription as description }; 33 | -------------------------------------------------------------------------------- /cli/lib/commands/codegen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { mkdirSync, readFileSync, writeFileSync } = require('node:fs'); 3 | const { join, resolve } = require('node:path'); 4 | const crdgen = require('@kubenode/crdgen'); 5 | const yaml = require('js-yaml'); 6 | const kCommand = 'codegen'; 7 | const kDescription = 'Generate code from project resources'; 8 | const flags = { 9 | group: { 10 | type: 'string', 11 | multiple: false, 12 | short: 'g', 13 | description: 'API group for the CRD.' 14 | }, 15 | kind: { 16 | type: 'string', 17 | multiple: false, 18 | short: 'k', 19 | description: 'Kind for the CRD.' 20 | }, 21 | version: { 22 | type: 'string', 23 | multiple: false, 24 | short: 'v', 25 | description: 'API version for the CRD.' 26 | } 27 | }; 28 | 29 | // eslint-disable-next-line require-await 30 | async function run(flags, positionals) { 31 | if (flags.group === undefined) { 32 | throw new Error('--group must be specified'); 33 | } 34 | 35 | if (flags.kind === undefined) { 36 | throw new Error('--kind must be specified'); 37 | } 38 | 39 | if (flags.version === undefined) { 40 | throw new Error('--version must be specified'); 41 | } 42 | 43 | const projectDir = resolve(flags.directory); 44 | const gvDirName = `${flags.group}_${flags.version}`.toLowerCase(); 45 | const ctrlDir = join(projectDir, 'lib', 'controller', gvDirName); 46 | const typePath = join(ctrlDir, `${flags.kind.toLowerCase()}_types.ts`); 47 | const crdDir = join(projectDir, 'config', 'crd'); 48 | const crdKustomizationFile = join(crdDir, 'kustomization.yaml'); 49 | const kustomizationFile = join(projectDir, 'config', 'kustomization.yaml'); 50 | const models = crdgen.generateModelsFromFiles([typePath]); 51 | const crdYamlFiles = []; 52 | 53 | mkdirSync(crdDir, { recursive: true }); 54 | 55 | for (const [kind, model] of models) { 56 | const filename = `${gvDirName}_${kind.toLowerCase()}.yaml`; 57 | const fullname = join(crdDir, filename); 58 | const crd = model.toCRD(); 59 | const crdYaml = yaml.dump(crd); 60 | 61 | writeFileSync(fullname, crdYaml); 62 | crdYamlFiles.push(filename); 63 | } 64 | 65 | createOrUpdateCRDKustomizationFile(crdKustomizationFile, crdYamlFiles); 66 | updateKustomizationFile(kustomizationFile); 67 | } 68 | 69 | function updateKustomizationFile(filename) { 70 | const yamlData = readFileSync(filename, 'utf8'); 71 | const body = yaml.load(yamlData, { filename }); 72 | 73 | // @ts-ignore 74 | body.resources ??= []; 75 | // @ts-ignore 76 | const resources = body.resources; 77 | 78 | if (resources.includes('crd')) { 79 | return; 80 | } 81 | 82 | resources.splice(resources.indexOf('rbac'), 0, 'crd'); 83 | writeFileSync(filename, yaml.dump(body)); 84 | } 85 | 86 | function createOrUpdateCRDKustomizationFile(filename, crdFiles) { 87 | let body; 88 | 89 | try { 90 | const yamlData = readFileSync(filename, 'utf8'); 91 | body = yaml.load(yamlData, { filename }); 92 | } catch (err) { 93 | body = { 94 | apiVersion: 'kustomize.config.k8s.io/v1beta1', 95 | kind: 'Kustomization' 96 | }; 97 | } 98 | 99 | // @ts-ignore 100 | body.resources ??= []; 101 | 102 | for (let i = 0; i < crdFiles.length; ++i) { 103 | const file = crdFiles[i]; 104 | 105 | // @ts-ignore 106 | if (!body.resources.includes(file)) { 107 | // @ts-ignore 108 | body.resources.push(file); 109 | } 110 | } 111 | 112 | writeFileSync(filename, yaml.dump(body)); 113 | } 114 | 115 | module.exports = { 116 | name: kCommand, 117 | description: kDescription, 118 | flags, 119 | run 120 | }; 121 | -------------------------------------------------------------------------------- /cli/lib/commands/configure.d.ts: -------------------------------------------------------------------------------- 1 | declare const kCommand: "configure"; 2 | declare const kDescription: "Configure project settings"; 3 | export function subcommands(): Map Promise; 7 | }>; 8 | export { kCommand as name, kDescription as description }; 9 | -------------------------------------------------------------------------------- /cli/lib/commands/configure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const kCommand = 'configure'; 3 | const kDescription = 'Configure project settings'; 4 | 5 | function subcommands() { 6 | const managerImage = require('./configure_manager_image'); 7 | const commands = new Map([ 8 | [managerImage.name, managerImage] 9 | ]); 10 | 11 | return commands; 12 | } 13 | 14 | module.exports = { 15 | name: kCommand, 16 | description: kDescription, 17 | subcommands 18 | }; 19 | -------------------------------------------------------------------------------- /cli/lib/commands/configure_manager_image.d.ts: -------------------------------------------------------------------------------- 1 | declare const kCommand: "manager-image"; 2 | declare const kDescription: "Configure the manager image"; 3 | export function run(flags: any, positionals: any): Promise; 4 | export { kCommand as name, kDescription as description }; 5 | -------------------------------------------------------------------------------- /cli/lib/commands/configure_manager_image.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { readFileSync, writeFileSync } = require('node:fs'); 3 | const { join, resolve } = require('node:path'); 4 | const { parse: parseReference } = require('@kubenode/reference'); 5 | const yaml = require('js-yaml'); 6 | const { Project } = require('../project'); 7 | const kCommand = 'manager-image'; 8 | const kDescription = 'Configure the manager image'; 9 | 10 | // eslint-disable-next-line require-await 11 | async function run(flags, positionals) { 12 | if (positionals.length !== 3) { 13 | throw new Error('incorrect arguments. expected image argument only'); 14 | } 15 | 16 | const reference = positionals[2]; 17 | const refObject = parseReference(reference); 18 | const projectDir = resolve(flags.directory); 19 | const packageJson = join(projectDir, 'package.json'); 20 | const managerDir = join(projectDir, 'config', 'manager'); 21 | const managerKustomization = join(managerDir, 'kustomization.yaml'); 22 | 23 | Project.fromDirectory(projectDir); 24 | updatePackageJsonScripts(packageJson, reference); 25 | updateManagerKustomization(managerKustomization, refObject); 26 | } 27 | 28 | function updatePackageJsonScripts(filename, reference) { 29 | const json = JSON.parse(readFileSync(filename, 'utf8')); 30 | json.scripts ??= {}; 31 | json.scripts['docker-build'] = `docker build . -t ${reference}`; 32 | json.scripts['docker-push'] = `docker push ${reference}`; 33 | writeFileSync(filename, JSON.stringify(json, null, 2)); 34 | } 35 | 36 | function updateManagerKustomization(filename, reference) { 37 | const yamlData = readFileSync(filename, 'utf8'); 38 | const body = yaml.load(yamlData, { filename }); 39 | 40 | // @ts-ignore 41 | body.images ??= []; 42 | // @ts-ignore 43 | const { images } = body; 44 | const controller = images.find((image) => { 45 | return image?.name === 'controller'; 46 | }); 47 | 48 | if (controller === undefined) { 49 | throw new Error(`'controller' image not found in '${filename}'`); 50 | } 51 | 52 | const { domain, path } = reference.namedRepository; 53 | const newName = domain ? `${domain}/${path}` : path; 54 | const newTag = reference.tag ?? 'latest'; 55 | 56 | controller.newName = newName; 57 | controller.newTag = newTag; 58 | 59 | writeFileSync(filename, yaml.dump(body)); 60 | } 61 | 62 | module.exports = { 63 | name: kCommand, 64 | description: kDescription, 65 | run 66 | }; 67 | -------------------------------------------------------------------------------- /cli/lib/commands/init.d.ts: -------------------------------------------------------------------------------- 1 | declare const kCommand: "init"; 2 | declare const kDescription: "Initialize a new project"; 3 | export const flags: { 4 | domain: { 5 | type: string; 6 | multiple: boolean; 7 | short: string; 8 | description: string; 9 | }; 10 | 'project-name': { 11 | type: string; 12 | multiple: boolean; 13 | short: string; 14 | default: string; 15 | description: string; 16 | }; 17 | }; 18 | export function run(flags: any, positionals: any): Promise; 19 | export { kCommand as name, kDescription as description }; 20 | -------------------------------------------------------------------------------- /cli/lib/commands/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { mkdirSync, writeFileSync } = require('node:fs'); 3 | const { basename, join, resolve } = require('node:path'); 4 | const { Project } = require('../project'); 5 | const kCommand = 'init'; 6 | const kDescription = 'Initialize a new project'; 7 | const kProjectVersion = '0'; 8 | const flags = { 9 | domain: { 10 | type: 'string', 11 | multiple: false, 12 | short: 'd', 13 | description: 'Domain for CRDs in the project.' 14 | }, 15 | 'project-name': { 16 | type: 'string', 17 | multiple: false, 18 | short: 'p', 19 | default: basename(process.cwd()) ?? 'kubenode-project', 20 | description: 'Name of the project. Default: current directory name' 21 | } 22 | }; 23 | let templates; 24 | 25 | // eslint-disable-next-line require-await 26 | async function run(flags, positionals) { 27 | if (flags.domain === undefined) { 28 | throw new Error('--domain must be specified'); 29 | } 30 | 31 | const projectDir = resolve(flags.directory); 32 | const srcDir = join(projectDir, 'lib'); 33 | const packageJson = join(projectDir, 'package.json'); 34 | const dockerfile = join(projectDir, 'Dockerfile'); 35 | const main = join(srcDir, 'index.js'); 36 | const mainTest = join(srcDir, 'index.test.js'); 37 | const managerDir = join(projectDir, 'config', 'manager'); 38 | const managerConfig = join(managerDir, 'manager.yaml'); 39 | const managerKustomization = join(managerDir, 'kustomization.yaml'); 40 | const rbacDir = join(projectDir, 'config', 'rbac'); 41 | const rbacConfig = join(rbacDir, 'manager_role.yaml'); 42 | const rbacKustomization = join(rbacDir, 'kustomization.yaml'); 43 | const kustomizationFile = join(projectDir, 'config', 'kustomization.yaml'); 44 | const data = { 45 | domain: flags.domain, 46 | projectName: flags['project-name'], 47 | projectPath: projectDir, 48 | version: kProjectVersion 49 | }; 50 | const project = new Project(data); 51 | 52 | lazyLoadTemplates(); 53 | mkdirSync(srcDir, { recursive: true }); 54 | mkdirSync(managerDir, { recursive: true }); 55 | mkdirSync(rbacDir, { recursive: true }); 56 | writeFileSync(dockerfile, templates.dockerfile(data)); 57 | writeFileSync(packageJson, templates.packageJson(data)); 58 | writeFileSync(main, templates.main(data)); 59 | writeFileSync(mainTest, templates.mainTest(data)); 60 | writeFileSync(managerConfig, templates.manager(data)); 61 | writeFileSync(managerKustomization, templates.managerKustomization(data)); 62 | writeFileSync(rbacConfig, templates.managerRole(data)); 63 | writeFileSync(rbacKustomization, templates.rbacKustomization(data)); 64 | writeFileSync(kustomizationFile, templates.kustomization(data)); 65 | project.write(); 66 | } 67 | 68 | function lazyLoadTemplates() { 69 | templates = require('../templates/init'); 70 | } 71 | 72 | module.exports = { 73 | name: kCommand, 74 | description: kDescription, 75 | flags, 76 | run 77 | }; 78 | -------------------------------------------------------------------------------- /cli/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export function run(args: any): Promise; 2 | -------------------------------------------------------------------------------- /cli/lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const rootCommand = { 3 | name: 'kubenode', 4 | description: 'Kubernetes tools for Node.js', 5 | subcommands() { 6 | const add = require('./commands/add'); 7 | const codegen = require('./commands/codegen'); 8 | const configure = require('./commands/configure'); 9 | const init = require('./commands/init'); 10 | // @ts-ignore 11 | const commands = new Map([ 12 | [add.name, add], 13 | [codegen.name, codegen], 14 | [configure.name, configure], 15 | [init.name, init] 16 | ]); 17 | 18 | return commands; 19 | }, 20 | globalFlags: { 21 | directory: { 22 | type: 'string', 23 | multiple: false, 24 | short: 'w', 25 | default: process.cwd(), 26 | description: 'The working directory. Default: process.cwd()' 27 | } 28 | } 29 | }; 30 | 31 | 32 | async function run(args) { 33 | const { parse } = await import('@kubenode/argparser'); 34 | const { command, flags, positionals } = parse(rootCommand, args); 35 | 36 | return command.run(flags, positionals); 37 | } 38 | 39 | module.exports = { run }; 40 | -------------------------------------------------------------------------------- /cli/lib/project.d.ts: -------------------------------------------------------------------------------- 1 | export class Project { 2 | static fromDirectory(directory: any): Project; 3 | constructor(data: any); 4 | domain: string; 5 | markers: any; 6 | projectName: string; 7 | projectPath: string; 8 | version: string; 9 | resources: any[]; 10 | ensureResource(r: any): any; 11 | getResource(group: any, version: any, kind: any): any; 12 | inject(filename: any, marker: any, str: any): void; 13 | toJSON(): { 14 | domain: string; 15 | projectName: string; 16 | version: string; 17 | resources: any[]; 18 | }; 19 | write(): void; 20 | writeMarkers(): Promise; 21 | } 22 | -------------------------------------------------------------------------------- /cli/lib/rbac.d.ts: -------------------------------------------------------------------------------- 1 | export function updateManagerRole(filename: any, data: any): void; 2 | -------------------------------------------------------------------------------- /cli/lib/rbac.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { readFileSync, writeFileSync } = require('node:fs'); 3 | const { isDeepStrictEqual } = require('node:util'); 4 | const pluralize = require('pluralize'); 5 | const yaml = require('js-yaml'); 6 | 7 | function updateManagerRole(filename, data) { 8 | const yamlData = readFileSync(filename, 'utf8'); 9 | const body = yaml.loadAll(yamlData, null, { filename }); 10 | const role = body.find((m) => { 11 | // @ts-ignore 12 | return m?.kind === 'ClusterRole' && 13 | // @ts-ignore 14 | m?.metadata?.name === `${data.projectName}-manager-role`; 15 | }); 16 | 17 | if (role === undefined) { 18 | throw new Error(`could not find manager cluster role in '${filename}'`); 19 | } 20 | 21 | // @ts-ignore 22 | role.rules ??= []; 23 | const plural = pluralize(data.kind.toLowerCase()); 24 | const status = `${plural}/status`; 25 | // @ts-ignore 26 | const rule = role.rules.find((r) => { 27 | return isDeepStrictEqual(r?.apiGroups, [data.group]) && 28 | isDeepStrictEqual(r?.resources, [plural, status]); 29 | }); 30 | 31 | if (rule === undefined) { 32 | // @ts-ignore 33 | role.rules.push({ 34 | apiGroups: [data.group], 35 | resources: [plural, status], 36 | verbs: ['create', 'delete', 'get', 'list', 'patch', 'update', 'watch'] 37 | }); 38 | } 39 | 40 | const documents = body.map((doc) => { 41 | return yaml.dump(doc); 42 | }); 43 | const manifestYaml = documents.join('---\n'); 44 | writeFileSync(filename, manifestYaml); 45 | } 46 | 47 | module.exports = { updateManagerRole }; 48 | -------------------------------------------------------------------------------- /cli/lib/templates/add_api.d.ts: -------------------------------------------------------------------------------- 1 | export function controller(data: any): string; 2 | export function sample(data: any): string; 3 | export function types(data: any): string; 4 | -------------------------------------------------------------------------------- /cli/lib/templates/add_api.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function controller(data) { 4 | return `import { 5 | newControllerManagedBy, 6 | Reconciler, 7 | } from '@kubenode/controller-runtime'; 8 | 9 | // ATTENTION: YOU **SHOULD** EDIT THIS FILE! 10 | 11 | export class ${data.kind}Reconciler extends Reconciler { 12 | async reconcile(ctx, req) { 13 | 14 | } 15 | 16 | setupWithManager(manager) { 17 | newControllerManagedBy(manager) 18 | .for('${data.kind}', '${data.group}/${data.version}') 19 | .complete(this); 20 | } 21 | } 22 | `; 23 | } 24 | 25 | function sample(data) { 26 | return `apiVersion: ${data.group}/${data.version} 27 | kind: ${data.kind} 28 | metadata: 29 | labels: 30 | app.kubernetes.io/name: ${data.projectName} 31 | app.kubernetes.io/managed-by: kubenode 32 | name: sample-${data.singular} 33 | namespace: 34 | spec: 35 | # TODO: Add fields here 36 | `; 37 | } 38 | 39 | function types(data) { 40 | // Note, the `metadata` field does not have a @description because it causes 41 | // an error when creating the CRD in the cluster. 42 | return `/** 43 | * @kubenode 44 | * @apiVersion ${data.group}/${data.version} 45 | * @kind ${data.kind} 46 | * @scope namespaced 47 | * @plural ${data.plural} 48 | * @singular ${data.singular} 49 | * @description Schema for the ${data.plural} API 50 | */ 51 | type ${data.kind} = { 52 | /** 53 | * @description apiVersion defines the versioned schema of this representation 54 | * of an object. Servers should convert recognized schemas to the latest 55 | * internal value, and may reject unrecognized values. 56 | */ 57 | apiVersion: string; 58 | 59 | /** 60 | * @description kind is a string value representing the REST resource this 61 | * object represents. 62 | */ 63 | kind: string; 64 | 65 | /** 66 | * metadata is a standard Kubernetes object for metadata. 67 | */ 68 | metadata: object; 69 | 70 | /** 71 | * @description spec defines the desired state of ${data.kind}. 72 | */ 73 | spec: ${data.kind}Spec; 74 | 75 | /** 76 | * @description status defines the observed state of ${data.kind}. 77 | */ 78 | status: ${data.kind}Status; 79 | }; 80 | 81 | type ${data.kind}Spec = { 82 | }; 83 | 84 | type ${data.kind}Status = { 85 | }; 86 | 87 | type ${data.listKind} = { 88 | /** 89 | * @description apiVersion defines the versioned schema of this representation 90 | * of an object. Servers should convert recognized schemas to the latest 91 | * internal value, and may reject unrecognized values. 92 | */ 93 | apiVersion: string; 94 | 95 | /** 96 | * @description kind is a string value representing the REST resource this 97 | * object represents. 98 | */ 99 | kind: string; 100 | 101 | /** 102 | * @description metadata is a standard Kubernetes object for metadata. 103 | */ 104 | metadata: object; 105 | 106 | /** 107 | * @description list contains the collection of ${data.kind} resources. 108 | */ 109 | items: ${data.kind}[]; 110 | }; 111 | `; 112 | } 113 | 114 | module.exports = { 115 | controller, 116 | sample, 117 | types 118 | }; 119 | -------------------------------------------------------------------------------- /cli/lib/templates/add_webhook.d.ts: -------------------------------------------------------------------------------- 1 | export function certificate(data: any): string; 2 | export function certManagerKustomization(data: any): string; 3 | export function service(data: any): string; 4 | export function webhook(data: any): string; 5 | export function webhookKustomization(data: any): string; 6 | -------------------------------------------------------------------------------- /cli/lib/templates/add_webhook.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function certificate(data) { 4 | return `apiVersion: cert-manager.io/v1 5 | kind: Issuer 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: ${data.projectName} 9 | app.kubernetes.io/managed-by: kubenode 10 | name: selfsigned-issuer 11 | namespace: ${data.projectName} 12 | spec: 13 | selfSigned: {} 14 | --- 15 | apiVersion: cert-manager.io/v1 16 | kind: Certificate 17 | metadata: 18 | labels: 19 | app.kubernetes.io/name: certificate 20 | app.kubernetes.io/instance: serving-cert 21 | app.kubernetes.io/component: certificate 22 | app.kubernetes.io/created-by: ${data.projectName} 23 | app.kubernetes.io/part-of: ${data.projectName} 24 | app.kubernetes.io/managed-by: kubenode 25 | name: serving-cert 26 | namespace: ${data.projectName} 27 | spec: 28 | dnsNames: 29 | - webhook-service.${data.projectName}.svc 30 | - webhook-service.${data.projectName}.svc.cluster.local 31 | issuerRef: 32 | kind: Issuer 33 | name: selfsigned-issuer 34 | secretName: webhook-server-cert 35 | `; 36 | } 37 | 38 | function certManagerKustomization(data) { 39 | return `apiVersion: kustomize.config.k8s.io/v1beta1 40 | kind: Kustomization 41 | resources: 42 | - certificate.yaml 43 | `; 44 | } 45 | 46 | function service(data) { 47 | return `apiVersion: v1 48 | kind: Service 49 | metadata: 50 | labels: 51 | app.kubernetes.io/name: ${data.projectName} 52 | app.kubernetes.io/managed-by: kubenode 53 | name: webhook-service 54 | namespace: ${data.projectName} 55 | spec: 56 | ports: 57 | - port: 443 58 | protocol: TCP 59 | targetPort: 9443 60 | selector: 61 | control-plane: controller-manager 62 | `; 63 | } 64 | 65 | function webhook(data) { 66 | const dashGroup = data.group.replaceAll('.', '-'); 67 | const path = `/${data.op}-${dashGroup}-${data.version}-${data.kind}`.toLowerCase(); 68 | 69 | return `import { webhook } from '@kubenode/controller-runtime'; 70 | 71 | // ATTENTION: YOU **SHOULD** EDIT THIS FILE! 72 | 73 | export class ${data.className} { 74 | constructor() { 75 | this.path = '${path}'; 76 | } 77 | 78 | handler(context, request) { 79 | return webhook.admission.allowed(); 80 | } 81 | 82 | setupWebhookWithManager(manager) { 83 | const handler = this.handler.bind(this); 84 | 85 | manager.getWebhookServer().register(this.path, handler); 86 | } 87 | } 88 | `; 89 | } 90 | 91 | function webhookKustomization(data) { 92 | return `apiVersion: kustomize.config.k8s.io/v1beta1 93 | kind: Kustomization 94 | resources: 95 | - service.yaml 96 | - manifests.yaml 97 | `; 98 | } 99 | 100 | module.exports = { 101 | certificate, 102 | certManagerKustomization, 103 | service, 104 | webhook, 105 | webhookKustomization 106 | }; 107 | -------------------------------------------------------------------------------- /cli/lib/templates/init.d.ts: -------------------------------------------------------------------------------- 1 | export function dockerfile(data: any): string; 2 | export function kustomization(data: any): string; 3 | export function main(data: any): string; 4 | export function mainTest(data: any): string; 5 | export function manager(data: any): string; 6 | export function managerKustomization(data: any): string; 7 | export function managerRole(data: any): string; 8 | export function packageJson(data: any): string; 9 | export function rbacKustomization(data: any): string; 10 | -------------------------------------------------------------------------------- /cli/lib/templates/init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const cliPackageJson = require('@kubenode/controller-runtime/package.json'); 3 | 4 | function dockerfile(data) { 5 | return `FROM node:22-alpine 6 | WORKDIR /usr/app 7 | COPY package.json package.json 8 | COPY lib lib 9 | ENV NODE_ENV production 10 | RUN npm install 11 | USER node 12 | CMD ["node", "lib/index.js"] 13 | `; 14 | } 15 | 16 | function kustomization(data) { 17 | return `apiVersion: kustomize.config.k8s.io/v1beta1 18 | kind: Kustomization 19 | resources: 20 | - manager 21 | - rbac 22 | `; 23 | } 24 | 25 | function main(data) { 26 | return `import { Manager } from '@kubenode/controller-runtime'; 27 | // @kubenode:scaffold:imports 28 | 29 | // ATTENTION: YOU **SHOULD** EDIT THIS FILE! 30 | 31 | const manager = new Manager(); 32 | // @kubenode:scaffold:manager 33 | await manager.start(); 34 | `; 35 | } 36 | 37 | function mainTest(data) { 38 | return `import { test } from 'node:test'; 39 | 40 | // ATTENTION: YOU **SHOULD** EDIT THIS FILE! 41 | 42 | test('verify application functionality', () => { 43 | }); 44 | `; 45 | } 46 | 47 | function manager(data) { 48 | return `apiVersion: v1 49 | kind: Namespace 50 | metadata: 51 | name: ${data.projectName} 52 | labels: 53 | control-plane: controller-manager 54 | app.kubernetes.io/name: namespace 55 | app.kubernetes.io/instance: ${data.projectName} 56 | app.kubernetes.io/component: manager 57 | app.kubernetes.io/created-by: ${data.projectName} 58 | app.kubernetes.io/part-of: ${data.projectName} 59 | app.kubernetes.io/managed-by: kubenode 60 | --- 61 | apiVersion: v1 62 | kind: ServiceAccount 63 | metadata: 64 | name: controller-manager 65 | namespace: ${data.projectName} 66 | labels: 67 | app.kubernetes.io/name: serviceaccount 68 | app.kubernetes.io/instance: controller-manager-sa 69 | app.kubernetes.io/component: rbac 70 | app.kubernetes.io/created-by: ${data.projectName} 71 | app.kubernetes.io/part-of: ${data.projectName} 72 | app.kubernetes.io/managed-by: kubenode 73 | --- 74 | apiVersion: apps/v1 75 | kind: Deployment 76 | metadata: 77 | name: controller-manager 78 | namespace: ${data.projectName} 79 | labels: 80 | control-plane: controller-manager 81 | app.kubernetes.io/name: deployment 82 | app.kubernetes.io/instance: controller-manager 83 | app.kubernetes.io/component: manager 84 | app.kubernetes.io/created-by: ${data.projectName} 85 | app.kubernetes.io/part-of: ${data.projectName} 86 | app.kubernetes.io/managed-by: kubenode 87 | spec: 88 | selector: 89 | matchLabels: 90 | control-plane: controller-manager 91 | replicas: 1 92 | revisionHistoryLimit: 2 93 | template: 94 | metadata: 95 | annotations: 96 | kubectl.kubernetes.io/default-container: manager 97 | labels: 98 | control-plane: controller-manager 99 | spec: 100 | securityContext: 101 | runAsNonRoot: true 102 | runAsUser: 1000 103 | seccompProfile: 104 | type: RuntimeDefault 105 | containers: 106 | - command: 107 | - node 108 | args: 109 | - lib/index.js 110 | image: controller:latest 111 | imagePullPolicy: Always 112 | name: manager 113 | securityContext: 114 | allowPrivilegeEscalation: false 115 | capabilities: 116 | drop: 117 | - "ALL" 118 | resources: 119 | limits: 120 | cpu: 500m 121 | memory: 256Mi 122 | requests: 123 | cpu: 10m 124 | memory: 128Mi 125 | serviceAccountName: controller-manager 126 | terminationGracePeriodSeconds: 10 127 | `; 128 | } 129 | 130 | function managerKustomization(data) { 131 | return `apiVersion: kustomize.config.k8s.io/v1beta1 132 | kind: Kustomization 133 | resources: 134 | - manager.yaml 135 | images: 136 | - name: controller 137 | newName: controller 138 | newTag: latest 139 | `; 140 | } 141 | 142 | function managerRole(data) { 143 | return `apiVersion: rbac.authorization.k8s.io/v1 144 | kind: ClusterRole 145 | metadata: 146 | name: ${data.projectName}-manager-role 147 | --- 148 | apiVersion: rbac.authorization.k8s.io/v1 149 | kind: ClusterRoleBinding 150 | metadata: 151 | name: ${data.projectName}-manager-rolebinding 152 | labels: 153 | app.kubernetes.io/name: clusterrolebinding 154 | app.kubernetes.io/instance: manager-rolebinding 155 | app.kubernetes.io/component: rbac 156 | app.kubernetes.io/created-by: ${data.projectName} 157 | app.kubernetes.io/part-of: ${data.projectName} 158 | app.kubernetes.io/managed-by: kubenode 159 | roleRef: 160 | apiGroup: rbac.authorization.k8s.io 161 | kind: ClusterRole 162 | name: ${data.projectName}-manager-role 163 | subjects: 164 | - kind: ServiceAccount 165 | name: controller-manager 166 | namespace: ${data.projectName} 167 | `; 168 | } 169 | 170 | function packageJson(data) { 171 | const pkg = { 172 | name: data.projectName, 173 | version: '1.0.0', 174 | description: '', 175 | type: 'module', 176 | scripts: { 177 | deploy: 'kubectl kustomize config | kubectl apply -f -', 178 | undeploy: 'kubectl kustomize config | kubectl delete --ignore-not-found -f -', 179 | 'docker-build': 'docker build . -t controller', 180 | 'docker-push': 'docker push controller', 181 | start: 'node lib/index.js', 182 | test: 'node --test' 183 | }, 184 | dependencies: { 185 | '@kubenode/controller-runtime': `^${cliPackageJson.version}` 186 | } 187 | }; 188 | 189 | return JSON.stringify(pkg, null, 2); 190 | } 191 | 192 | function rbacKustomization(data) { 193 | return `apiVersion: kustomize.config.k8s.io/v1beta1 194 | kind: Kustomization 195 | resources: 196 | - manager_role.yaml 197 | `; 198 | } 199 | 200 | module.exports = { 201 | dockerfile, 202 | kustomization, 203 | main, 204 | mainTest, 205 | manager, 206 | managerKustomization, 207 | managerRole, 208 | packageJson, 209 | rbacKustomization 210 | }; 211 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kubenode/cli", 3 | "version": "0.1.10", 4 | "description": "Kubenode CLI", 5 | "license": "MIT", 6 | "author": "Colin J. Ihrig (http://www.cjihrig.com/)", 7 | "main": "lib/index.js", 8 | "scripts": { 9 | "lint": "belly-button -f", 10 | "pretest": "npm run lint && tsc --noEmit --resolveJsonModule", 11 | "test": "node --test --experimental-test-coverage", 12 | "types": "rm lib/*.d.ts && rm lib/commands/*.d.ts && rm lib/templates/*.d.ts && tsc --resolveJsonModule" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^22.7.5", 16 | "belly-button": "^8.0.0", 17 | "typescript": "^5.6.2" 18 | }, 19 | "homepage": "https://github.com/cjihrig/kubenode#readme", 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/cjihrig/kubenode.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/cjihrig/kubenode/issues" 26 | }, 27 | "keywords": [ 28 | "kubernetes", 29 | "k8s" 30 | ], 31 | "directories": { 32 | "lib": "lib", 33 | "test": "test" 34 | }, 35 | "dependencies": { 36 | "@kubenode/argparser": "^0.1.1", 37 | "@kubenode/controller-runtime": "^0.4.2", 38 | "@kubenode/crdgen": "^0.1.1", 39 | "@kubenode/reference": "^0.1.1", 40 | "js-yaml": "^4.1.0", 41 | "pluralize": "^8.0.0" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "module": "commonjs", 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "target": "es2022" 11 | }, 12 | "include": ["lib/**/*.js"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /controller-runtime/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /controller-runtime/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | -------------------------------------------------------------------------------- /controller-runtime/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Colin Ihrig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /controller-runtime/lib/apimachinery/errors.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * reasonForError() returns the reason for a particular error. 3 | * @param {Error} err 4 | * @returns {string} A StatusReason representing the cause of the error. 5 | */ 6 | export function reasonForError(err: Error): string; 7 | /** 8 | * reasonAndCodeForError() returns the reason and code for a particular error. 9 | * @param {Error} err 10 | * @returns {[string, number]} An array containing the reason and code of the 11 | * error. 12 | */ 13 | export function reasonAndCodeForError(err: Error): [string, number]; 14 | /** 15 | * isAlreadyExists() determines if the err is an error which indicates that a 16 | * specified resource already exists. 17 | * @param {Error} err 18 | * @returns {boolean} 19 | */ 20 | export function isAlreadyExists(err: Error): boolean; 21 | /** 22 | * isBadRequest() determines if err is an error which indicates that the request 23 | * is invalid. 24 | * @param {Error} err 25 | * @returns {boolean} 26 | */ 27 | export function isBadRequest(err: Error): boolean; 28 | /** 29 | * isConflict() determines if the err is an error which indicates the provided 30 | * update conflicts. 31 | * @param {Error} err 32 | * @returns {boolean} 33 | */ 34 | export function isConflict(err: Error): boolean; 35 | /** 36 | * isForbidden() determines if err is an error which indicates that the request 37 | * is forbidden and cannot be completed as requested. 38 | * @param {Error} err 39 | * @returns {boolean} 40 | */ 41 | export function isForbidden(err: Error): boolean; 42 | /** 43 | * isGone() is true if the error indicates the requested resource is no longer 44 | * available. 45 | * @param {Error} err 46 | * @returns {boolean} 47 | */ 48 | export function isGone(err: Error): boolean; 49 | /** 50 | * isInternalError() determines if err is an error which indicates an internal 51 | * server error. 52 | * @param {Error} err 53 | * @returns {boolean} 54 | */ 55 | export function isInternalError(err: Error): boolean; 56 | /** 57 | * isInvalid() determines if the err is an error which indicates the provided 58 | * resource is not valid. 59 | * @param {Error} err 60 | * @returns {boolean} 61 | */ 62 | export function isInvalid(err: Error): boolean; 63 | /** 64 | * isMethodNotSupported() determines if the err is an error which indicates the 65 | * provided action could not be performed because it is not supported by the 66 | * server. 67 | * @param {Error} err 68 | * @returns {boolean} 69 | */ 70 | export function isMethodNotSupported(err: Error): boolean; 71 | /** 72 | * isNotAcceptable() determines if err is an error which indicates that the 73 | * request failed due to an invalid Accept header. 74 | * @param {Error} err 75 | * @returns {boolean} 76 | */ 77 | export function isNotAcceptable(err: Error): boolean; 78 | /** 79 | * isNotFound() determines if an err indicating that a resource could not be 80 | * found. 81 | * @param {Error} err 82 | * @returns {boolean} 83 | */ 84 | export function isNotFound(err: Error): boolean; 85 | /** 86 | * isRequestEntityTooLargeError() determines if err is an error which indicates 87 | * the request entity is too large. 88 | * @param {Error} err 89 | * @returns {boolean} 90 | */ 91 | export function isRequestEntityTooLargeError(err: Error): boolean; 92 | /** 93 | * isResourceExpired() is true if the error indicates the resource has expired 94 | * and the current action is no longer possible. 95 | * @param {Error} err 96 | * @returns {boolean} 97 | */ 98 | export function isResourceExpired(err: Error): boolean; 99 | /** 100 | * isServerTimeout() determines if err is an error which indicates that the 101 | * request needs to be retried by the client. 102 | * @param {Error} err 103 | * @returns {boolean} 104 | */ 105 | export function isServerTimeout(err: Error): boolean; 106 | /** 107 | * isServiceUnavailable() is true if the error indicates the underlying service 108 | * is no longer available. 109 | * @param {Error} err 110 | * @returns {boolean} 111 | */ 112 | export function isServiceUnavailable(err: Error): boolean; 113 | /** 114 | * isTimeout() determines if err is an error which indicates that request times 115 | * out due to long processing. 116 | * @param {Error} err 117 | * @returns {boolean} 118 | */ 119 | export function isTimeout(err: Error): boolean; 120 | /** 121 | * isTooManyRequests() determines if err is an error which indicates that there 122 | * are too many requests that the server cannot handle. 123 | * @param {Error} err 124 | * @returns {boolean} 125 | */ 126 | export function isTooManyRequests(err: Error): boolean; 127 | /** 128 | * isUnauthorized() determines if err is an error which indicates that the 129 | * request is unauthorized and requires authentication by the user. 130 | * @param {Error} err 131 | * @returns {boolean} 132 | */ 133 | export function isUnauthorized(err: Error): boolean; 134 | /** 135 | * isUnsupportedMediaType() determines if err is an error which indicates that 136 | * the request failed due to an invalid Content-Type header. 137 | * @param {Error} err 138 | * @returns {boolean} 139 | */ 140 | export function isUnsupportedMediaType(err: Error): boolean; 141 | declare namespace _default { 142 | export { isAlreadyExists }; 143 | export { isBadRequest }; 144 | export { isConflict }; 145 | export { isForbidden }; 146 | export { isGone }; 147 | export { isInternalError }; 148 | export { isInvalid }; 149 | export { isMethodNotSupported }; 150 | export { isNotAcceptable }; 151 | export { isNotFound }; 152 | export { isRequestEntityTooLargeError }; 153 | export { isResourceExpired }; 154 | export { isServerTimeout }; 155 | export { isServiceUnavailable }; 156 | export { isTimeout }; 157 | export { isTooManyRequests }; 158 | export { isUnauthorized }; 159 | export { isUnsupportedMediaType }; 160 | export { reasonForError }; 161 | } 162 | export default _default; 163 | -------------------------------------------------------------------------------- /controller-runtime/lib/apimachinery/meta/v1.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace _default { 2 | let StatusReasonUnknown: string; 3 | let StatusReasonUnauthorized: string; 4 | let StatusReasonForbidden: string; 5 | let StatusReasonNotFound: string; 6 | let StatusReasonAlreadyExists: string; 7 | let StatusReasonConflict: string; 8 | let StatusReasonGone: string; 9 | let StatusReasonInvalid: string; 10 | let StatusReasonServerTimeout: string; 11 | let StatusReasonTimeout: string; 12 | let StatusReasonTooManyRequests: string; 13 | let StatusReasonBadRequest: string; 14 | let StatusReasonMethodNotAllowed: string; 15 | let StatusReasonNotAcceptable: string; 16 | let StatusReasonRequestEntityTooLarge: string; 17 | let StatusReasonUnsupportedMediaType: string; 18 | let StatusReasonInternalError: string; 19 | let StatusReasonExpired: string; 20 | let StatusReasonServiceUnavailable: string; 21 | } 22 | export default _default; 23 | -------------------------------------------------------------------------------- /controller-runtime/lib/apimachinery/types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NamespacedName comprises a resource name and namespace rendered as "namespace/name". 3 | */ 4 | export class NamespacedName { 5 | /** 6 | * Construct a NamespacedName. 7 | * @param {string} name - Resource name. 8 | * @param {string} [namespace] - Resource namespace. 9 | */ 10 | constructor(name: string, namespace?: string); 11 | name: string; 12 | namespace: string; 13 | /** 14 | * Converts a NamespacedName to a string of the form "namespace/name". 15 | * @returns {string} A string representation of the NamespacedName. 16 | */ 17 | toString(): string; 18 | } 19 | declare namespace _default { 20 | export { NamespacedName }; 21 | export { kSeparator as separator }; 22 | export { kJSONPatchType as JSONPatchType }; 23 | export { kMergePatchType as MergePatchType }; 24 | export { kStrategicMergePatchType as StrategicMergePatchType }; 25 | export { kApplyPatchType as ApplyPatchType }; 26 | } 27 | export default _default; 28 | declare const kSeparator: "/"; 29 | declare const kJSONPatchType: "application/json-patch+json"; 30 | declare const kMergePatchType: "application/merge-patch+json"; 31 | declare const kStrategicMergePatchType: "application/strategic-merge-patch+json"; 32 | declare const kApplyPatchType: "application/apply-patch+yaml"; 33 | export { kSeparator as separator, kJSONPatchType as JSONPatchType, kMergePatchType as MergePatchType, kStrategicMergePatchType as StrategicMergePatchType, kApplyPatchType as ApplyPatchType }; 34 | -------------------------------------------------------------------------------- /controller-runtime/lib/apimachinery/types.js: -------------------------------------------------------------------------------- 1 | const kSeparator = '/'; 2 | const kJSONPatchType = 'application/json-patch+json'; 3 | const kMergePatchType = 'application/merge-patch+json'; 4 | const kStrategicMergePatchType = 'application/strategic-merge-patch+json'; 5 | const kApplyPatchType = 'application/apply-patch+yaml'; 6 | 7 | /** 8 | * NamespacedName comprises a resource name and namespace rendered as "namespace/name". 9 | */ 10 | export class NamespacedName { 11 | /** 12 | * Construct a NamespacedName. 13 | * @param {string} name - Resource name. 14 | * @param {string} [namespace] - Resource namespace. 15 | */ 16 | constructor(name, namespace = '') { 17 | this.name = name; 18 | this.namespace = namespace; 19 | } 20 | 21 | /** 22 | * Converts a NamespacedName to a string of the form "namespace/name". 23 | * @returns {string} A string representation of the NamespacedName. 24 | */ 25 | toString() { 26 | return `${this.namespace}${kSeparator}${this.name}`; 27 | } 28 | } 29 | 30 | export { 31 | kSeparator as separator, 32 | kJSONPatchType as JSONPatchType, 33 | kMergePatchType as MergePatchType, 34 | kStrategicMergePatchType as StrategicMergePatchType, 35 | kApplyPatchType as ApplyPatchType 36 | }; 37 | 38 | export default { 39 | NamespacedName, 40 | separator: kSeparator, 41 | JSONPatchType: kJSONPatchType, 42 | MergePatchType: kMergePatchType, 43 | StrategicMergePatchType: kStrategicMergePatchType, 44 | ApplyPatchType: kApplyPatchType 45 | }; 46 | -------------------------------------------------------------------------------- /controller-runtime/lib/builder.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} InternalGVK 3 | * @property {string} kind The resource kind. 4 | * @property {string} apiVersion The resource API version. 5 | */ 6 | /** 7 | * Builder exposes simple patterns for building common Controllers. 8 | */ 9 | export class Builder { 10 | /** 11 | * controllerManagedBy() returns a new Controller Builder that is managed by 12 | * the provided Manager. 13 | * @param {Manager} manager - The Manager to use. 14 | * @returns {Builder} 15 | */ 16 | static controllerManagedBy(manager: Manager): Builder; 17 | /** 18 | * Construct a Builder. 19 | * @param {Manager} manager - The Manager to use. 20 | */ 21 | constructor(manager: Manager); 22 | /** 23 | * build() creates and returns a Controller. 24 | * @param {Reconciler} reconciler - The Reconciler for the new Controller. 25 | * @returns {Controller} 26 | */ 27 | build(reconciler: Reconciler): Controller; 28 | /** 29 | * complete() creates a Controller. 30 | * @param {Reconciler} reconciler - The Reconciler for the new Controller. 31 | */ 32 | complete(reconciler: Reconciler): void; 33 | /** 34 | * for() defines the type of resource being reconciled, and configures the 35 | * Controller to respond to events by reconciling. 36 | * @param {string} kind - The resource kind 37 | * @param {string} apiVersion - The resource API version. 38 | * @returns {this} 39 | */ 40 | for(kind: string, apiVersion: string): this; 41 | /** 42 | * named() sets the name of the Controller to the given name. The name shows 43 | * up in metrics, among other things, and thus should be a Prometheus 44 | * compatible name (underscores and alphanumeric characters only). 45 | * @param {string} name - The name of the Controller. 46 | * @returns {this} 47 | */ 48 | named(name: string): this; 49 | /** 50 | * owns() defines types of resources being generated by the Controller, and 51 | * configures the Controller to respond to events by reconciling the owner 52 | * object. 53 | * @param {string} kind - The resource kind 54 | * @param {string} apiVersion - The resource API version. 55 | * @returns {this} 56 | */ 57 | owns(kind: string, apiVersion: string): this; 58 | /** 59 | * watches() This method is incomplete. 60 | * @returns {this} 61 | */ 62 | watches(): this; 63 | #private; 64 | } 65 | declare namespace _default { 66 | export { Builder }; 67 | } 68 | export default _default; 69 | export type InternalGVK = { 70 | /** 71 | * The resource kind. 72 | */ 73 | kind: string; 74 | /** 75 | * The resource API version. 76 | */ 77 | apiVersion: string; 78 | }; 79 | import { Reconciler } from './reconcile.js'; 80 | import { Controller } from './controller.js'; 81 | import { Manager } from './manager.js'; 82 | -------------------------------------------------------------------------------- /controller-runtime/lib/builder.js: -------------------------------------------------------------------------------- 1 | import { Controller } from './controller.js'; 2 | import { Manager } from './manager.js'; 3 | import { Reconciler } from './reconcile.js'; 4 | import { Source } from './source.js'; 5 | 6 | /** 7 | * @typedef {Object} InternalGVK 8 | * @property {string} kind The resource kind. 9 | * @property {string} apiVersion The resource API version. 10 | */ 11 | 12 | /** 13 | * Builder exposes simple patterns for building common Controllers. 14 | */ 15 | export class Builder { 16 | /** @type Manager */ 17 | #manager; 18 | /** @type InternalGVK | null */ 19 | #forInput; 20 | /** @type string */ 21 | #name; 22 | /** @type InternalGVK[] */ 23 | #ownsInput; 24 | 25 | /** 26 | * Construct a Builder. 27 | * @param {Manager} manager - The Manager to use. 28 | */ 29 | constructor(manager) { 30 | if (!(manager instanceof Manager)) { 31 | throw new TypeError('manager must be a Manager instance'); 32 | } 33 | 34 | this.#manager = manager; 35 | this.#name = ''; 36 | this.#forInput = null; 37 | this.#ownsInput = []; 38 | } 39 | 40 | /** 41 | * build() creates and returns a Controller. 42 | * @param {Reconciler} reconciler - The Reconciler for the new Controller. 43 | * @returns {Controller} 44 | */ 45 | build(reconciler) { 46 | if (!(reconciler instanceof Reconciler)) { 47 | throw new TypeError('reconciler must be a Reconciler instance'); 48 | } 49 | 50 | const ctrlName = this.#getControllerName(); 51 | const controller = new Controller(ctrlName, { reconciler }); 52 | this.#manager.add(controller); 53 | this.#setupControllerWatches(controller); 54 | return controller; 55 | } 56 | 57 | /** 58 | * complete() creates a Controller. 59 | * @param {Reconciler} reconciler - The Reconciler for the new Controller. 60 | */ 61 | complete(reconciler) { 62 | this.build(reconciler); 63 | } 64 | 65 | /** 66 | * for() defines the type of resource being reconciled, and configures the 67 | * Controller to respond to events by reconciling. 68 | * @param {string} kind - The resource kind 69 | * @param {string} apiVersion - The resource API version. 70 | * @returns {this} 71 | */ 72 | for(kind, apiVersion) { 73 | if (this.#forInput !== null) { 74 | throw new Error('for() can only be called once'); 75 | } 76 | 77 | this.#forInput = { kind, apiVersion }; 78 | return this; 79 | } 80 | 81 | /** 82 | * named() sets the name of the Controller to the given name. The name shows 83 | * up in metrics, among other things, and thus should be a Prometheus 84 | * compatible name (underscores and alphanumeric characters only). 85 | * @param {string} name - The name of the Controller. 86 | * @returns {this} 87 | */ 88 | named(name) { 89 | this.#name = name; 90 | return this; 91 | } 92 | 93 | /** 94 | * owns() defines types of resources being generated by the Controller, and 95 | * configures the Controller to respond to events by reconciling the owner 96 | * object. 97 | * @param {string} kind - The resource kind 98 | * @param {string} apiVersion - The resource API version. 99 | * @returns {this} 100 | */ 101 | owns(kind, apiVersion) { 102 | // TODO(cjihrig): Implement this. 103 | throw new Error('unimplemented'); 104 | // this.#ownsInput.push({ kind, apiVersion }); 105 | // return this; 106 | } 107 | 108 | /** 109 | * watches() This method is incomplete. 110 | * @returns {this} 111 | */ 112 | watches() { 113 | // TODO(cjihrig): Implement this. 114 | throw new Error('unimplemented'); 115 | // return this; 116 | } 117 | 118 | /** 119 | * controllerManagedBy() returns a new Controller Builder that is managed by 120 | * the provided Manager. 121 | * @param {Manager} manager - The Manager to use. 122 | * @returns {Builder} 123 | */ 124 | static controllerManagedBy(manager) { 125 | return new Builder(manager); 126 | } 127 | 128 | /** 129 | * getControllerName() returns the name to use for the Controller. If a name 130 | * has been explicitly provided, that is used. Otherwise, the name is based on 131 | * the resource kind of the builder. 132 | * @returns {string} 133 | */ 134 | #getControllerName() { 135 | if (this.#name) { 136 | return this.#name; 137 | } 138 | 139 | if (this.#forInput) { 140 | return this.#forInput.kind.toLowerCase(); 141 | } 142 | 143 | throw new Error('controller has no name. for() or named() must be called'); 144 | } 145 | 146 | /** 147 | * setupControllerWatches() configures the necessary watchers for the 148 | * Controller. 149 | * @param {Controller} ctrl - The Controller to configure. 150 | */ 151 | #setupControllerWatches(ctrl) { 152 | if (this.#forInput !== null) { 153 | const src = new Source( 154 | this.#manager.kubeconfig, 155 | this.#manager.client, 156 | this.#forInput.kind, 157 | this.#forInput.apiVersion 158 | ); 159 | ctrl.watch(src); 160 | } 161 | } 162 | } 163 | 164 | export default { Builder }; 165 | -------------------------------------------------------------------------------- /controller-runtime/lib/client.d.ts: -------------------------------------------------------------------------------- 1 | export class Client { 2 | } 3 | declare namespace _default { 4 | export { Client }; 5 | } 6 | export default _default; 7 | -------------------------------------------------------------------------------- /controller-runtime/lib/client.js: -------------------------------------------------------------------------------- 1 | export class Client {} 2 | 3 | export default { Client }; 4 | -------------------------------------------------------------------------------- /controller-runtime/lib/context.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Context carries deadlines, cancellation signals, and other values across API 3 | * boundaries. 4 | */ 5 | export class Context { 6 | /** 7 | * Context.create() creates a new context object. 8 | * @returns {Context} 9 | */ 10 | static create(): Context; 11 | /** 12 | * Construct a Context. 13 | */ 14 | constructor(key: any, parent: any); 15 | /** 16 | * cancel() aborts the context. 17 | */ 18 | cancel(): void; 19 | /** 20 | * child() derives a child context from the current context. 21 | * @returns {Context} 22 | */ 23 | child(): Context; 24 | /** 25 | * A Promise that is settled based on the state of the context. 26 | * @type {Promise} 27 | */ 28 | get done(): Promise; 29 | /** 30 | * An AbortSignal that aborts when the context is cancelled. 31 | * @type {AbortSignal} 32 | */ 33 | get signal(): AbortSignal; 34 | /** 35 | * A Map of arbitrary user data stored on the context. 36 | * @type {Map} 37 | */ 38 | get values(): Map; 39 | #private; 40 | } 41 | /** 42 | * ReconcileContext extends Context to provide additional data that is useful 43 | * during reconciliation. 44 | */ 45 | export class ReconcileContext extends Context { 46 | /** 47 | * ReconcileContext.fromContext() creates a new ReconcileContext object. 48 | * @param {Context} context - Parent context. 49 | * @param {string} reconcileID - Unique ID for the reconciliation. 50 | * @returns {ReconcileContext} 51 | */ 52 | static fromContext(context: Context, reconcileID: string): ReconcileContext; 53 | /** 54 | * Construct a ReconcileContext. 55 | */ 56 | constructor(key: any, parent: any, reconcileID: any); 57 | /** @type string */ 58 | reconcileID: string; 59 | /** 60 | * child() derives a child ReconcileContext from the current ReconcileContext. 61 | * @returns {ReconcileContext} 62 | */ 63 | child(): ReconcileContext; 64 | } 65 | declare namespace _default { 66 | export { Context }; 67 | export { ReconcileContext }; 68 | } 69 | export default _default; 70 | export type PromiseWithResolvers = import("./util.js").PromiseWithResolvers; 71 | -------------------------------------------------------------------------------- /controller-runtime/lib/context.js: -------------------------------------------------------------------------------- 1 | import { withResolvers } from './util.js'; 2 | 3 | /** 4 | * @typedef {import('./util.js').PromiseWithResolvers} PromiseWithResolvers 5 | */ 6 | 7 | const kConstructorKey = Symbol('constructorKey'); 8 | const noop = () => {}; 9 | 10 | /** 11 | * Context carries deadlines, cancellation signals, and other values across API 12 | * boundaries. 13 | */ 14 | export class Context { 15 | /** @type AbortController */ 16 | #controller; 17 | /** @type Context|null */ 18 | #parent; 19 | /** @type PromiseWithResolvers */ 20 | #promise; 21 | /** @type boolean */ 22 | #settled; 23 | /** @type AbortSignal */ 24 | #signal; 25 | /** @type Map */ 26 | #values; 27 | 28 | /** 29 | * Construct a Context. 30 | */ 31 | constructor(key, parent) { 32 | if (key !== kConstructorKey) { 33 | throw new Error('illegal constructor'); 34 | } 35 | 36 | this.#controller = new AbortController(); 37 | this.#parent = parent; 38 | this.#promise = withResolvers(); 39 | this.#settled = false; 40 | this.#values = null; 41 | 42 | if (parent === null) { 43 | this.#signal = this.#controller.signal; 44 | } else { 45 | this.#signal = AbortSignal.any([ 46 | this.#controller.signal, 47 | this.#parent.#signal, 48 | ]); 49 | } 50 | 51 | // Prevent unhandledRejections on aborts. 52 | this.#promise.promise.catch(noop); 53 | 54 | this.#signal.addEventListener('abort', () => { 55 | if (this.#settled) { 56 | return; 57 | } 58 | 59 | this.#settled = true; 60 | this.#promise.reject(this.#signal.reason); 61 | }); 62 | } 63 | 64 | /** 65 | * cancel() aborts the context. 66 | */ 67 | cancel() { 68 | this.#controller.abort(); 69 | } 70 | 71 | /** 72 | * child() derives a child context from the current context. 73 | * @returns {Context} 74 | */ 75 | child() { 76 | return new Context(kConstructorKey, this); 77 | } 78 | 79 | /** 80 | * A Promise that is settled based on the state of the context. 81 | * @type {Promise} 82 | */ 83 | get done() { 84 | return this.#promise.promise; 85 | } 86 | 87 | /** 88 | * An AbortSignal that aborts when the context is cancelled. 89 | * @type {AbortSignal} 90 | */ 91 | get signal() { 92 | return this.#signal; 93 | } 94 | 95 | /** 96 | * A Map of arbitrary user data stored on the context. 97 | * @type {Map} 98 | */ 99 | get values() { 100 | if (this.#values === null) { 101 | if (this.#parent === null) { 102 | this.#values = new Map(); 103 | } else { 104 | this.#values = new Map(this.#parent.#values); 105 | } 106 | } 107 | 108 | return this.#values; 109 | } 110 | 111 | /** 112 | * Context.create() creates a new context object. 113 | * @returns {Context} 114 | */ 115 | static create() { 116 | return new Context(kConstructorKey, null); 117 | } 118 | } 119 | 120 | /** 121 | * ReconcileContext extends Context to provide additional data that is useful 122 | * during reconciliation. 123 | */ 124 | export class ReconcileContext extends Context { 125 | /** 126 | * Construct a ReconcileContext. 127 | */ 128 | constructor(key, parent, reconcileID) { 129 | super(key, parent); 130 | /** @type string */ 131 | this.reconcileID = reconcileID; 132 | } 133 | 134 | /** 135 | * child() derives a child ReconcileContext from the current ReconcileContext. 136 | * @returns {ReconcileContext} 137 | */ 138 | child() { 139 | return new ReconcileContext(kConstructorKey, this, this.reconcileID); 140 | } 141 | 142 | /** 143 | * ReconcileContext.fromContext() creates a new ReconcileContext object. 144 | * @param {Context} context - Parent context. 145 | * @param {string} reconcileID - Unique ID for the reconciliation. 146 | * @returns {ReconcileContext} 147 | */ 148 | static fromContext(context, reconcileID) { 149 | if (!(context instanceof Context)) { 150 | throw new TypeError('context must be a Context instance'); 151 | } 152 | 153 | if (typeof reconcileID !== 'string') { 154 | throw new TypeError('reconcileID must be a string'); 155 | } 156 | 157 | return new ReconcileContext(kConstructorKey, context, reconcileID); 158 | } 159 | } 160 | 161 | export default { 162 | Context, 163 | ReconcileContext, 164 | } 165 | -------------------------------------------------------------------------------- /controller-runtime/lib/controller.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./context.js').Context} Context 3 | * @typedef {import('./reconcile.js').Request} Request 4 | * @typedef {import('./source.js').Source} Source 5 | * 6 | * @typedef {Object} ControllerOptions 7 | * @property {Reconciler} reconciler The reconciler for the controller. 8 | */ 9 | /** 10 | * Controllers use events to trigger reconcile requests. 11 | */ 12 | export class Controller { 13 | /** 14 | * Construct a Controller. 15 | * @param {string} name - Controller name. 16 | * @param {ControllerOptions} [options] - Configuration options. 17 | */ 18 | constructor(name: string, options?: ControllerOptions); 19 | /** 20 | * The controller name. 21 | * @type {string} 22 | */ 23 | get name(): string; 24 | /** 25 | * reconcile() invokes the controller's reconciler for a specific resource. 26 | * @param {Context} context - Context to use. 27 | * @param {Request} request - Resource information to reconcile. 28 | * @returns {Promise} 29 | */ 30 | reconcile(context: Context, request: Request): Promise; 31 | /** 32 | * start() begins consuming events. 33 | * @param {Context} context - Context to use. 34 | */ 35 | start(context: Context): void; 36 | /** 37 | * A boolean indicating if the controller was started. 38 | * @type {boolean} 39 | */ 40 | get started(): boolean; 41 | /** 42 | * stop() causes the controller to stop consuming events. If the controller 43 | * was already stopped, this is a no-op. 44 | * @returns {Promise} 45 | */ 46 | stop(): Promise; 47 | /** 48 | * watch() begins watching a Source for events. 49 | * @param {Source} source - Source to watch for events. 50 | */ 51 | watch(source: Source): void; 52 | #private; 53 | } 54 | declare namespace _default { 55 | export { Controller }; 56 | } 57 | export default _default; 58 | export type Context = import("./context.js").Context; 59 | export type Request = import("./reconcile.js").Request; 60 | export type Source = import("./source.js").Source; 61 | export type ControllerOptions = { 62 | /** 63 | * The reconciler for the controller. 64 | */ 65 | reconciler: Reconciler; 66 | }; 67 | import { Result } from './reconcile.js'; 68 | import { Reconciler } from './reconcile.js'; 69 | -------------------------------------------------------------------------------- /controller-runtime/lib/controller.js: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto'; 2 | import { ReconcileContext } from './context.js'; 3 | import { Reconciler, Result, TerminalError } from './reconcile.js'; 4 | import { Queue } from './queue.js'; 5 | 6 | /** 7 | * @typedef {import('./context.js').Context} Context 8 | * @typedef {import('./reconcile.js').Request} Request 9 | * @typedef {import('./source.js').Source} Source 10 | * 11 | * @typedef {Object} ControllerOptions 12 | * @property {Reconciler} reconciler The reconciler for the controller. 13 | */ 14 | 15 | /** 16 | * Controllers use events to trigger reconcile requests. 17 | */ 18 | export class Controller { 19 | /** @type Context */ 20 | #context; 21 | /** @type boolean */ 22 | #isReconciling; 23 | /** @type string */ 24 | #name; 25 | /** @type Queue */ 26 | #queue; 27 | /** @type Reconciler */ 28 | #reconciler; 29 | /** @type Source[] */ 30 | #sources; 31 | /** @type boolean */ 32 | #started; 33 | /** @type Source[] */ 34 | #startWatches; 35 | 36 | /** 37 | * Construct a Controller. 38 | * @param {string} name - Controller name. 39 | * @param {ControllerOptions} [options] - Configuration options. 40 | */ 41 | constructor(name, options) { 42 | if (typeof name !== 'string') { 43 | throw new TypeError('name must be a string'); 44 | } 45 | 46 | if (options === null || typeof options !== 'object') { 47 | throw new TypeError('options must be an object'); 48 | } 49 | 50 | const { 51 | reconciler 52 | } = options; 53 | 54 | if (!(reconciler instanceof Reconciler)) { 55 | throw new TypeError('options.reconciler must be a Reconciler instance'); 56 | } 57 | 58 | this.#context = null; 59 | this.#isReconciling = false; 60 | this.#name = name; 61 | this.#queue = new Queue(); 62 | this.#reconciler = reconciler; 63 | this.#sources = []; 64 | this.#started = false; 65 | this.#startWatches = []; 66 | } 67 | 68 | /** 69 | * The controller name. 70 | * @type {string} 71 | */ 72 | get name() { 73 | return this.#name; 74 | } 75 | 76 | /** 77 | * reconcile() invokes the controller's reconciler for a specific resource. 78 | * @param {Context} context - Context to use. 79 | * @param {Request} request - Resource information to reconcile. 80 | * @returns {Promise} 81 | */ 82 | reconcile(context, request) { 83 | const ctx = ReconcileContext.fromContext(context, randomUUID()); 84 | 85 | return this.#reconciler.reconcile(ctx, request); 86 | } 87 | 88 | /** 89 | * start() begins consuming events. 90 | * @param {Context} context - Context to use. 91 | */ 92 | start(context) { 93 | if (this.#started) { 94 | throw new Error('controller already started'); 95 | } 96 | 97 | this.#started = true; 98 | this.#context = context; 99 | 100 | this.#queue.on('data', async () => { 101 | if (this.#isReconciling) { 102 | return; 103 | } 104 | 105 | while (true) { 106 | const data = this.#queue.dequeue(); 107 | 108 | if (data === undefined) { 109 | break; 110 | } 111 | 112 | await this.#reconcileHandler(context, data.data); 113 | } 114 | }); 115 | 116 | context.signal.addEventListener('abort', () => { 117 | this.stop(); 118 | }); 119 | 120 | for (let i = 0; i < this.#startWatches.length; ++i) { 121 | const source = this.#startWatches[i]; 122 | // TODO(cjihrig): Probably need to implement 123 | // Source.prototype.waitForSync() and call that here instead. 124 | this.#sources.push(source); 125 | source.start(this.#context.child(), this.#queue); 126 | } 127 | 128 | this.#startWatches = []; 129 | } 130 | 131 | /** 132 | * A boolean indicating if the controller was started. 133 | * @type {boolean} 134 | */ 135 | get started() { 136 | return this.#started; 137 | } 138 | 139 | /** 140 | * stop() causes the controller to stop consuming events. If the controller 141 | * was already stopped, this is a no-op. 142 | * @returns {Promise} 143 | */ 144 | async stop() { 145 | if (!this.#started) { 146 | return; 147 | } 148 | 149 | const promises = this.#sources.map((source) => { 150 | return source.stop(); 151 | }); 152 | 153 | await Promise.allSettled(promises); 154 | this.#started = false; 155 | } 156 | 157 | /** 158 | * watch() begins watching a Source for events. 159 | * @param {Source} source - Source to watch for events. 160 | */ 161 | watch(source) { 162 | if (!this.#started) { 163 | this.#startWatches.push(source); 164 | return; 165 | } 166 | 167 | this.#sources.push(source); 168 | source.start(this.#context.child(), this.#queue); 169 | } 170 | 171 | /** 172 | * reconcileHandler() is an internal method that invokes the reconciler, and 173 | * processes the result, including requeuing logic. 174 | * @param {Context} context - Context to use. 175 | * @param {Request} request - Resource information to reconcile. 176 | * @returns {Promise} 177 | */ 178 | async #reconcileHandler(context, request) { 179 | let result; 180 | 181 | this.#isReconciling = true; 182 | 183 | try { 184 | result = await this.reconcile(context, request); 185 | 186 | if (!(result instanceof Result)) { 187 | result = new Result(result); 188 | } 189 | 190 | if (result.requeueAfter > 0) { 191 | setTimeout(() => { 192 | this.#queue.enqueue(request); 193 | }, result.requeueAfter); 194 | } else if (result.requeue) { 195 | this.#queue.enqueue(request); 196 | } 197 | } catch (err) { 198 | if (!(err instanceof TerminalError)) { 199 | this.#queue.enqueue(request); 200 | } 201 | } 202 | 203 | this.#isReconciling = false; 204 | } 205 | } 206 | 207 | export default { Controller }; 208 | -------------------------------------------------------------------------------- /controller-runtime/lib/controllerutil.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * addFinalizer() adds a finalizer to a Kubernetes object. If the finalizer 3 | * already existed on the object, it is not added again. Returns a boolean 4 | * indicating if the finalizer was added. 5 | * @param {KubernetesObject} o - Kubernetes object to add the finalizer to. 6 | * @param {string} finalizer - Finalizer to add. 7 | * @returns {boolean} 8 | */ 9 | export function addFinalizer(o: KubernetesObject, finalizer: string): boolean; 10 | /** 11 | * containsFinalizer() returns a boolean indicating if the object contains the 12 | * specified finalizer. 13 | * @param {KubernetesObject} o - Kubernetes object to check. 14 | * @param {string} finalizer - Finalizer to check. 15 | * @returns {boolean} 16 | */ 17 | export function containsFinalizer(o: KubernetesObject, finalizer: string): boolean; 18 | /** 19 | * removeFinalizer() removes the specified finalizer from the object if it is 20 | * present. A boolean is returned indicated if the finalizer was removed. 21 | * @param {KubernetesObject} o - Kubernetes object to remove the finalizer from. 22 | * @param {string} finalizer - Finalizer to remove. 23 | * @returns {boolean} 24 | */ 25 | export function removeFinalizer(o: KubernetesObject, finalizer: string): boolean; 26 | /** 27 | * hasOwnerReference() returns true if the owners list contains an owner 28 | * reference that matches the object's group, kind, and name. 29 | * @param {V1OwnerReference[]} ownerRefs - List of owner references to check. 30 | * @param {KubernetesObject} o - Kubernetes object to get GVK from. 31 | * @returns {boolean} 32 | */ 33 | export function hasOwnerReference(ownerRefs: V1OwnerReference[], o: KubernetesObject): boolean; 34 | /** 35 | * setOwnerReference() ensures the given object contains an owner reference to 36 | * the provided owner object. If a reference to the same owner already exists, 37 | * it is overwritten. 38 | * @param {KubernetesObject} owner - Kubernetes object used as owner. 39 | * @param {KubernetesObject} object - Kubernetes object that is owned. 40 | */ 41 | export function setOwnerReference(owner: KubernetesObject, object: KubernetesObject): void; 42 | /** 43 | * removeOwnerReference() removes an owner reference from object. If no such 44 | * owner reference exists, an exception is thrown. 45 | * @param {KubernetesObject} owner - Kubernetes object used as owner. 46 | * @param {KubernetesObject} object - Kubernetes object that is owned. 47 | */ 48 | export function removeOwnerReference(owner: KubernetesObject, object: KubernetesObject): void; 49 | /** 50 | * hasControllerReference() returns true if the object has an owner reference 51 | * with the controller property set to true. 52 | * @param {KubernetesObject} o - Kubernetes object to check. 53 | * @returns {boolean} 54 | */ 55 | export function hasControllerReference(o: KubernetesObject): boolean; 56 | /** 57 | * setControllerReference() sets owner as a Controller OwnerReference on object. 58 | * @param {KubernetesObject} owner - Kubernetes object used as owner. 59 | * @param {KubernetesObject} object - Kubernetes object that is owned. 60 | */ 61 | export function setControllerReference(owner: KubernetesObject, object: KubernetesObject): void; 62 | /** 63 | * removeControllerReference() removes a controller owner reference from object. 64 | * If no such owner reference exists, an exception is thrown. 65 | * @param {KubernetesObject} o - Kubernetes object that is owned. 66 | */ 67 | export function removeControllerReference(o: KubernetesObject): void; 68 | /** 69 | * @typedef {import('@kubernetes/client-node').KubernetesObject} KubernetesObject 70 | * @typedef {import('@kubernetes/client-node').V1OwnerReference} V1OwnerReference 71 | */ 72 | /** 73 | * AlreadyOwnedError is an error thrown when trying to assign a controller to 74 | * an object that is already owned by another controller. 75 | */ 76 | export class AlreadyOwnedError extends Error { 77 | /** 78 | * Construct an AlreadyOwnedError. 79 | * @param {KubernetesObject} object - Owned Kubernetes object. 80 | * @param {V1OwnerReference} owner - Kubernetes owner reference. 81 | */ 82 | constructor(object: KubernetesObject, owner: V1OwnerReference); 83 | object: import("@kubernetes/client-node").KubernetesObject; 84 | owner: import("@kubernetes/client-node").V1OwnerReference; 85 | } 86 | declare namespace _default { 87 | export { AlreadyOwnedError }; 88 | export { addFinalizer }; 89 | export { containsFinalizer }; 90 | export { hasControllerReference }; 91 | export { hasOwnerReference }; 92 | export { removeControllerReference }; 93 | export { removeFinalizer }; 94 | export { removeOwnerReference }; 95 | export { setControllerReference }; 96 | export { setOwnerReference }; 97 | } 98 | export default _default; 99 | export type KubernetesObject = import("@kubernetes/client-node").KubernetesObject; 100 | export type V1OwnerReference = import("@kubernetes/client-node").V1OwnerReference; 101 | -------------------------------------------------------------------------------- /controller-runtime/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace _default { 2 | export { k8s }; 3 | export { apimachinery }; 4 | export { Context }; 5 | export { controllerutil }; 6 | export { Manager }; 7 | export { newControllerManagedBy }; 8 | export { Reconciler }; 9 | export { Request }; 10 | export { Result }; 11 | export { Source }; 12 | export { TerminalError }; 13 | export { webhook }; 14 | } 15 | export default _default; 16 | import * as k8s from '@kubernetes/client-node'; 17 | export namespace apimachinery { 18 | export { errors }; 19 | export namespace meta { 20 | export { metav1 as v1 }; 21 | } 22 | export { schema }; 23 | export { types }; 24 | } 25 | import { Context } from './context.js'; 26 | import controllerutil from './controllerutil.js'; 27 | import { Manager } from './manager.js'; 28 | export const newControllerManagedBy: typeof Builder.controllerManagedBy; 29 | import { Reconciler } from './reconcile.js'; 30 | import { Request } from './reconcile.js'; 31 | import { Result } from './reconcile.js'; 32 | import { Source } from './source.js'; 33 | import { TerminalError } from './reconcile.js'; 34 | export namespace webhook { 35 | export { admission }; 36 | export { Server }; 37 | } 38 | import errors from './apimachinery/errors.js'; 39 | import metav1 from './apimachinery/meta/v1.js'; 40 | import schema from './apimachinery/schema.js'; 41 | import types from './apimachinery/types.js'; 42 | import { Builder } from './builder.js'; 43 | import admission from './webhook/admission.js'; 44 | import { Server } from './webhook/server.js'; 45 | export { k8s, Context, controllerutil, Manager, Reconciler, Request, Result, Source, TerminalError }; 46 | -------------------------------------------------------------------------------- /controller-runtime/lib/index.js: -------------------------------------------------------------------------------- 1 | import * as k8s from '@kubernetes/client-node'; 2 | import errors from './apimachinery/errors.js'; 3 | import metav1 from './apimachinery/meta/v1.js'; 4 | import schema from './apimachinery/schema.js'; 5 | import types from './apimachinery/types.js'; 6 | import { Builder } from './builder.js'; 7 | import { Context } from './context.js'; 8 | import controllerutil from './controllerutil.js'; 9 | import { Manager } from './manager.js'; 10 | import { 11 | Reconciler, 12 | Request, 13 | Result, 14 | TerminalError 15 | } from './reconcile.js'; 16 | import admission from './webhook/admission.js'; 17 | import { Server } from './webhook/server.js'; 18 | import { Source } from './source.js'; 19 | 20 | const newControllerManagedBy = Builder.controllerManagedBy; 21 | const apimachinery = { 22 | errors, 23 | meta: { 24 | v1: metav1 25 | }, 26 | schema, 27 | types 28 | }; 29 | const webhook = { 30 | admission, 31 | Server 32 | }; 33 | 34 | export { 35 | k8s, 36 | apimachinery, 37 | Context, 38 | controllerutil, 39 | Manager, 40 | newControllerManagedBy, 41 | Reconciler, 42 | Request, 43 | Result, 44 | Source, 45 | TerminalError, 46 | webhook 47 | }; 48 | 49 | export default { 50 | k8s, 51 | apimachinery, 52 | Context, 53 | controllerutil, 54 | Manager, 55 | newControllerManagedBy, 56 | Reconciler, 57 | Request, 58 | Result, 59 | Source, 60 | TerminalError, 61 | webhook 62 | }; 63 | -------------------------------------------------------------------------------- /controller-runtime/lib/leaderelection/leaderelection.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LeaderElector implements a leader election client. 3 | */ 4 | export class LeaderElector { 5 | /** 6 | * Construct a LeaderElector. 7 | * @param {LeaderElectionConfig} options - Leader election configuration 8 | * options. 9 | */ 10 | constructor(options: LeaderElectionConfig); 11 | lock: LeaseLock; 12 | leaseDuration: number; 13 | renewDeadline: number; 14 | retryPeriod: number; 15 | callbacks: LeaderCallbacks; 16 | name: string; 17 | /** @type {LeaderElectionRecord} */ 18 | record: LeaderElectionRecord; 19 | observedTime: number; 20 | leaseValidUntil: number; 21 | /** 22 | * run() starts the leader election loop. 23 | * @param {Context} ctx - The context to use. 24 | * @returns {Promise} 25 | */ 26 | run(ctx: Context): Promise; 27 | /** 28 | * isLeader() returns true if the last observed leader was this client, and 29 | * false otherwise. 30 | * @returns {boolean} 31 | */ 32 | isLeader(): boolean; 33 | /** 34 | * isLeaseValid() returns true if the lease is valid at the time provided, 35 | * and false otherwise. 36 | * @param {number} time - The time to check the lease against. 37 | * @returns {boolean} 38 | */ 39 | isLeaseValid(time: number): boolean; 40 | #private; 41 | } 42 | declare namespace _default { 43 | export { LeaderElector }; 44 | } 45 | export default _default; 46 | export type LeaderElectionRecord = import("./leaselock.js").LeaderElectionRecord; 47 | export type LeaderCallbacks = { 48 | /** 49 | * Called when the 50 | * LeaderElector starts leading. 51 | */ 52 | onStartedLeading: (context: Context) => void; 53 | /** 54 | * Called when the LeaderElector stops 55 | * leading. This is also called when the LeaderElector exits, even if it did 56 | * not start leading. 57 | */ 58 | onStoppedLeading: () => void; 59 | /** 60 | * Called when the client 61 | * observes a leader that is not the previously observed leader. This includes 62 | * the first observed leader when the client starts. 63 | */ 64 | onNewLeader: (identity: string) => void; 65 | }; 66 | export type LeaderElectionConfig = { 67 | /** 68 | * The resource that will be used for locking. 69 | */ 70 | lock: LeaseLock; 71 | /** 72 | * A client needs to wait a full leaseDuration 73 | * without observing a change to the record before it can attempt to take over. 74 | * When all clients are shutdown and a new set of clients are started with 75 | * different names against the same leader record, they must wait the full 76 | * leaseDuration before attempting to acquire the lease. Thus, leaseDuration 77 | * should be as short as possible (within your tolerance for clock skew rate) 78 | * to avoid possible long waits. leaseDuration is measured in milliseconds. 79 | */ 80 | leaseDuration: number; 81 | /** 82 | * The number of milliseconds that the acting 83 | * leader will retry refreshing leadership before giving up. 84 | */ 85 | renewDeadline: number; 86 | /** 87 | * The number of milliseconds the client should 88 | * wait between tries of actions. 89 | */ 90 | retryPeriod: number; 91 | /** 92 | * Callbacks that are invoked at various 93 | * lifecycle events of the LeaderElector. 94 | */ 95 | callbacks: LeaderCallbacks; 96 | /** 97 | * The name of the resource lock. 98 | */ 99 | name: string; 100 | }; 101 | import { LeaseLock } from './leaselock.js'; 102 | import { Context } from '../context.js'; 103 | -------------------------------------------------------------------------------- /controller-runtime/lib/leaderelection/leaselock.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * LeaseLock implements distributed locking using Kubernetes leases. 3 | */ 4 | export class LeaseLock { 5 | /** 6 | * Construct a LeaseLock. leaseMeta should contain a name and namespace that 7 | * the leader elector will attempt to lead. 8 | * @param {V1ObjectMeta} leaseMeta - Kubernetes object meta. 9 | * @param {CoordinationV1Api} client - Kubernetes coordination v1 client. 10 | * @param {ResourceLockConfig} lockConfig - Configuration for the lock. 11 | */ 12 | constructor(leaseMeta: V1ObjectMeta, client: CoordinationV1Api, lockConfig: ResourceLockConfig); 13 | leaseMeta: import("@kubernetes/client-node").V1ObjectMeta; 14 | client: import("@kubernetes/client-node").CoordinationV1Api; 15 | lockConfig: ResourceLockConfig; 16 | /** @type {V1Lease} */ 17 | lease: V1Lease; 18 | /** 19 | * create() creates a new Kubernetes Lease object. 20 | * @param {LeaderElectionRecord} record - Configuration of the lease. 21 | * @returns {Promise} 22 | */ 23 | create(record: LeaderElectionRecord): Promise; 24 | /** 25 | * get() retrieves the Kubernetes Lease object corresponding to the lock. 26 | * @returns {Promise} 27 | */ 28 | get(): Promise; 29 | /** 30 | * update() updates the Kubernetes Lease object for the lock. 31 | * @param {LeaderElectionRecord} record - Configuration of the lease. 32 | * @returns {Promise} 33 | */ 34 | update(record: LeaderElectionRecord): Promise; 35 | /** 36 | * recordEvent() records events with additional metadata during leader 37 | * election. 38 | * @param {string} s - String representation of the event being recorded. 39 | */ 40 | recordEvent(s: string): void; 41 | /** 42 | * The identity of the lock. 43 | * @type {string} 44 | */ 45 | get identity(): string; 46 | /** 47 | * toString() returns a string representation of the LeaseLock. 48 | * @returns {string} 49 | */ 50 | toString(): string; 51 | } 52 | declare namespace _default { 53 | export { LeaseLock }; 54 | } 55 | export default _default; 56 | export type CoordinationV1Api = import("@kubernetes/client-node").CoordinationV1Api; 57 | export type KubernetesObject = import("@kubernetes/client-node").KubernetesObject; 58 | export type V1Lease = import("@kubernetes/client-node").V1Lease; 59 | export type V1MicroTime = import("@kubernetes/client-node").V1MicroTime; 60 | export type V1ObjectMeta = import("@kubernetes/client-node").V1ObjectMeta; 61 | export type EventRecorder = import("../record/recorder.js").EventRecorder; 62 | export type LeaderElectionRecord = { 63 | /** 64 | * The identity of the holder of a current 65 | * lease. 66 | */ 67 | holderIdentity: string; 68 | /** 69 | * Duration that candidates for a lease 70 | * need to wait to force acquire it. 71 | */ 72 | leaseDurationSeconds: number; 73 | /** 74 | * Time when the current lease was acquired. 75 | */ 76 | acquireTime: V1MicroTime; 77 | /** 78 | * Time when the current holder of a lease 79 | * has last updated the lease. 80 | */ 81 | renewTime: V1MicroTime; 82 | /** 83 | * The number of transitions of a lease 84 | * between holders. 85 | */ 86 | leaderTransitions: number; 87 | /** 88 | * The strategy for picking the leader for 89 | * coordinated leader election. 90 | */ 91 | strategy?: string; 92 | /** 93 | * Signals to a lease holder that the 94 | * lease has a more optimal holder and should be given up. 95 | */ 96 | preferredHolder?: string; 97 | }; 98 | export type ResourceLockConfig = { 99 | /** 100 | * Unique string identifying a lease holder across 101 | * all participants in an election. 102 | */ 103 | identity: string; 104 | /** 105 | * Used to record changes to the lock. 106 | */ 107 | eventRecorder?: EventRecorder; 108 | }; 109 | -------------------------------------------------------------------------------- /controller-runtime/lib/manager.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Manager runs controllers, webhooks, and other common dependencies. 3 | */ 4 | export class Manager { 5 | /** 6 | * Construct a Manager. 7 | * @param {ManagerOptions} [options] - Configuration options. 8 | */ 9 | constructor(options?: ManagerOptions); 10 | client: KubernetesObjectApi; 11 | kubeconfig: KubeConfig; 12 | /** 13 | * add() causes the Manager to manage the provided controller. 14 | * @param {Controller} controller The controller to manage. 15 | */ 16 | add(controller: Controller): void; 17 | /** 18 | * getWebhookServer() returns the Manager's webhook server. 19 | * @returns {Server} 20 | */ 21 | getWebhookServer(): Server; 22 | /** 23 | * start() starts the webhook server and all managed controllers. 24 | * @param {Context} context The context to use. 25 | * @returns {Promise} 26 | */ 27 | start(context?: Context): Promise; 28 | /** 29 | * A boolean indicating if the manager was started. 30 | * @type {boolean} 31 | */ 32 | get started(): boolean; 33 | /** 34 | * stop() causes the manager to stop all resources that it manages. If the 35 | * manager was already stopped, this is a no-op. 36 | * @returns {Promise} 37 | */ 38 | stop(): Promise; 39 | #private; 40 | } 41 | declare namespace _default { 42 | export { Manager }; 43 | } 44 | export default _default; 45 | export type Controller = import("./controller.js").Controller; 46 | export type PromiseWithResolvers = import("./util.js").PromiseWithResolvers; 47 | export type ManagerOptions = { 48 | /** 49 | * - Kubeconfig to use. 50 | */ 51 | kubeconfig?: KubeConfig; 52 | /** 53 | * - Coordination v1 API to use. 54 | */ 55 | coordinationClient?: CoordinationV1Api; 56 | /** 57 | * - Core v1 API to use. 58 | */ 59 | coreClient?: CoreV1Api; 60 | /** 61 | * - Kubernetes client to use. 62 | */ 63 | client?: KubernetesObjectApi; 64 | /** 65 | * - Whether or not to use leader election 66 | * when starting the manager. 67 | */ 68 | leaderElection?: boolean; 69 | /** 70 | * - The name of the resource that 71 | * leader election will use for holding the leader lock. 72 | */ 73 | leaderElectionName?: string; 74 | /** 75 | * - The namespace in which the 76 | * leader election resource will be created. 77 | */ 78 | leaderElectionNamespace?: string; 79 | /** 80 | * - The duration that non-leader candidates 81 | * will wait to force acquire leadership. This is measured against time of last 82 | * observed ack. Default is 15 seconds. 83 | */ 84 | leaseDuration?: number; 85 | /** 86 | * - The duration that the acting leader 87 | * will retry refreshing leadership before giving up. Default is ten seconds. 88 | */ 89 | renewDeadline?: number; 90 | /** 91 | * - The duration the LeaderElector clients 92 | * should wait between tries of actions. Default is two seconds. 93 | */ 94 | retryPeriod?: number; 95 | }; 96 | import { KubernetesObjectApi } from '@kubernetes/client-node'; 97 | import { KubeConfig } from '@kubernetes/client-node'; 98 | import { Server } from './webhook/server.js'; 99 | import { Context } from './context.js'; 100 | import { CoordinationV1Api } from '@kubernetes/client-node'; 101 | import { CoreV1Api } from '@kubernetes/client-node'; 102 | -------------------------------------------------------------------------------- /controller-runtime/lib/queue.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @template T 3 | * @typedef {Object} QueueData 4 | * @property {T} data The data stored in the Queue. 5 | */ 6 | /** 7 | * A queue data structure. 8 | * @template T 9 | */ 10 | export class Queue extends EventEmitter<[never]> { 11 | /** 12 | * Construct an empty Queue. 13 | */ 14 | constructor(); 15 | /** @type T[] */ 16 | data: T[]; 17 | /** 18 | * enqueue() adds an item to the back of the Queue and causes a 'data' event 19 | * to be emitted. 20 | * @param {T} item - Item to insert into the Queue. 21 | */ 22 | enqueue(item: T): void; 23 | /** 24 | * dequeue() adds an item to the back of the Queue and causes a 'data' event 25 | * @returns {QueueData|undefined} 26 | */ 27 | dequeue(): QueueData | undefined; 28 | } 29 | declare namespace _default { 30 | export { Queue }; 31 | } 32 | export default _default; 33 | export type QueueData = { 34 | /** 35 | * The data stored in the Queue. 36 | */ 37 | data: T; 38 | }; 39 | import { EventEmitter } from 'node:events'; 40 | -------------------------------------------------------------------------------- /controller-runtime/lib/queue.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'node:events'; 2 | 3 | /** 4 | * @template T 5 | * @typedef {Object} QueueData 6 | * @property {T} data The data stored in the Queue. 7 | */ 8 | 9 | /** 10 | * A queue data structure. 11 | * @template T 12 | */ 13 | export class Queue extends EventEmitter { 14 | /** 15 | * Construct an empty Queue. 16 | */ 17 | constructor() { 18 | super(); 19 | /** @type T[] */ 20 | this.data = []; 21 | } 22 | 23 | /** 24 | * enqueue() adds an item to the back of the Queue and causes a 'data' event 25 | * to be emitted. 26 | * @param {T} item - Item to insert into the Queue. 27 | */ 28 | enqueue(item) { 29 | this.data.push(item); 30 | process.nextTick(() => { 31 | this.emit('data'); 32 | }); 33 | } 34 | 35 | /** 36 | * dequeue() adds an item to the back of the Queue and causes a 'data' event 37 | * @returns {QueueData|undefined} 38 | */ 39 | dequeue() { 40 | if (this.data.length === 0) { 41 | return undefined; 42 | } 43 | 44 | const data = this.data.shift(); 45 | 46 | return { data }; 47 | } 48 | } 49 | 50 | export default { Queue }; 51 | -------------------------------------------------------------------------------- /controller-runtime/lib/reconcile.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./context.js').Context} Context 3 | */ 4 | /** 5 | * Request contains the information necessary to reconcile a Kubernetes object. 6 | * This includes the information to uniquely identify the object. It does not 7 | * contain information about any specific Event or the object contents itself. 8 | */ 9 | export class Request extends NamespacedName { 10 | } 11 | /** 12 | * Result contains the result of a Reconciler invocation. 13 | */ 14 | export class Result { 15 | /** 16 | * Construct a Result. 17 | * @param {any} requeue - Whether or not to requeue a reconciliation. 18 | */ 19 | constructor(requeue: any); 20 | requeue: boolean; 21 | requeueAfter: number; 22 | } 23 | /** 24 | * TerminalError is an error that will not be retried. 25 | */ 26 | export class TerminalError extends Error { 27 | /** 28 | * Construct a TerminalError. 29 | * @param {any} cause - The underlying cause of the TerminalError. 30 | */ 31 | constructor(cause: any); 32 | } 33 | /** 34 | * Reconciler provides an interface for implementing reconciliation. 35 | */ 36 | export class Reconciler { 37 | /** 38 | * reconcile() implementations compare the desired state of an object as 39 | * specified by the user against the actual cluster state, and then performs 40 | * operations to make the actual cluster state reflect the desired state. 41 | * @param {Context} context - The context of the reconciliation. 42 | * @param {Request} request - The requested resource information. 43 | * @returns {Promise} 44 | */ 45 | reconcile(context: Context, request: Request): Promise; 46 | } 47 | declare namespace _default { 48 | export { Reconciler }; 49 | export { Request }; 50 | export { Result }; 51 | export { TerminalError }; 52 | } 53 | export default _default; 54 | export type Context = import("./context.js").Context; 55 | import { NamespacedName } from './apimachinery/types.js'; 56 | -------------------------------------------------------------------------------- /controller-runtime/lib/reconcile.js: -------------------------------------------------------------------------------- 1 | import { NamespacedName } from './apimachinery/types.js'; 2 | 3 | /** 4 | * @typedef {import('./context.js').Context} Context 5 | */ 6 | 7 | /** 8 | * Request contains the information necessary to reconcile a Kubernetes object. 9 | * This includes the information to uniquely identify the object. It does not 10 | * contain information about any specific Event or the object contents itself. 11 | */ 12 | export class Request extends NamespacedName {} 13 | 14 | /** 15 | * Result contains the result of a Reconciler invocation. 16 | */ 17 | export class Result { 18 | /** 19 | * Construct a Result. 20 | * @param {any} requeue - Whether or not to requeue a reconciliation. 21 | */ 22 | constructor(requeue) { 23 | if (typeof requeue === 'number') { 24 | this.requeue = true; 25 | this.requeueAfter = requeue; 26 | } else { 27 | this.requeue = !!requeue; 28 | this.requeueAfter = 0; 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * TerminalError is an error that will not be retried. 35 | */ 36 | export class TerminalError extends Error { 37 | /** 38 | * Construct a TerminalError. 39 | * @param {any} cause - The underlying cause of the TerminalError. 40 | */ 41 | constructor(cause) { 42 | super('terminal error', { cause }); 43 | this.name = 'TerminalError'; 44 | } 45 | } 46 | 47 | /** 48 | * Reconciler provides an interface for implementing reconciliation. 49 | */ 50 | export class Reconciler { 51 | /** 52 | * reconcile() implementations compare the desired state of an object as 53 | * specified by the user against the actual cluster state, and then performs 54 | * operations to make the actual cluster state reflect the desired state. 55 | * @param {Context} context - The context of the reconciliation. 56 | * @param {Request} request - The requested resource information. 57 | * @returns {Promise} 58 | */ 59 | // eslint-disable-next-line class-methods-use-this, require-await 60 | async reconcile(context, request) { 61 | throw new Error('unimplemented reconcile()', { cause: request }); 62 | } 63 | } 64 | 65 | export default { 66 | Reconciler, 67 | Request, 68 | Result, 69 | TerminalError 70 | }; 71 | -------------------------------------------------------------------------------- /controller-runtime/lib/record/recorder.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * EventRecorder records events of an event source. 3 | */ 4 | export class EventRecorder { 5 | /** 6 | * Construct an EventRecorder. 7 | * @param {V1EventSource} source - The recorder's EventSource. 8 | * @param {CoreV1Api} client - The client to create events with. 9 | */ 10 | constructor(source: V1EventSource, client: CoreV1Api); 11 | /** 12 | * event() constructs an event from the given information and puts it in the 13 | * queue for sending. The resulting event will be created in the same 14 | * namespace as the reference object. 15 | * @param {KubernetesObject} object - The object this event is about. 16 | * @param {string} eventType - Can be 'Normal' or 'Warning'. New types could 17 | * be added in the future. 18 | * @param {string} reason - The reason the event was generated. The reason 19 | * should be short, unique, and written in UpperCamelCase. 20 | * @param {string} message - A human readable message. 21 | */ 22 | event(object: KubernetesObject, eventType: string, reason: string, message: string): Promise; 23 | #private; 24 | } 25 | declare namespace _default { 26 | export { EventRecorder }; 27 | } 28 | export default _default; 29 | export type CoreV1Api = import("@kubernetes/client-node").CoreV1Api; 30 | export type KubernetesObject = import("@kubernetes/client-node").KubernetesObject; 31 | export type V1EventSource = import("@kubernetes/client-node").V1EventSource; 32 | export type V1ObjectReference = import("@kubernetes/client-node").V1ObjectReference; 33 | -------------------------------------------------------------------------------- /controller-runtime/lib/record/recorder.js: -------------------------------------------------------------------------------- 1 | import { V1MicroTime } from '@kubernetes/client-node'; 2 | import { GroupVersionKind } from '../apimachinery/schema.js'; 3 | 4 | /** 5 | * @typedef {import('@kubernetes/client-node').CoreV1Api} CoreV1Api 6 | * @typedef {import('@kubernetes/client-node').KubernetesObject} KubernetesObject 7 | * @typedef {import('@kubernetes/client-node').V1EventSource} V1EventSource 8 | * @typedef {import('@kubernetes/client-node').V1ObjectReference} V1ObjectReference 9 | */ 10 | 11 | const kEventTypeNormal = 'Normal'; 12 | const kEventTypeWarning = 'Warning'; 13 | 14 | /** 15 | * EventRecorder records events of an event source. 16 | */ 17 | export class EventRecorder { 18 | /** @type CoreV1Api */ 19 | #client; 20 | /** @type V1EventSource */ 21 | #source; 22 | 23 | /** 24 | * Construct an EventRecorder. 25 | * @param {V1EventSource} source - The recorder's EventSource. 26 | * @param {CoreV1Api} client - The client to create events with. 27 | */ 28 | constructor(source, client) { 29 | this.#client = client; 30 | this.#source = source; 31 | } 32 | 33 | /** 34 | * event() constructs an event from the given information and puts it in the 35 | * queue for sending. The resulting event will be created in the same 36 | * namespace as the reference object. 37 | * @param {KubernetesObject} object - The object this event is about. 38 | * @param {string} eventType - Can be 'Normal' or 'Warning'. New types could 39 | * be added in the future. 40 | * @param {string} reason - The reason the event was generated. The reason 41 | * should be short, unique, and written in UpperCamelCase. 42 | * @param {string} message - A human readable message. 43 | */ 44 | async event(object, eventType, reason, message) { 45 | // TODO(cjihrig): This function should log errors instead of throwing. 46 | if (typeof eventType !== 'string') { 47 | throw new TypeError('eventType must be a string'); 48 | } 49 | 50 | if (eventType !== kEventTypeNormal && eventType !== kEventTypeWarning) { 51 | throw new Error(`eventType must be '${kEventTypeNormal}' or '${kEventTypeWarning}'`); 52 | } 53 | 54 | if (typeof reason !== 'string') { 55 | throw new TypeError('reason must be a string'); 56 | } 57 | 58 | if (typeof message !== 'string') { 59 | throw new TypeError('message must be a string'); 60 | } 61 | 62 | const ref = getReference(object); 63 | const now = new Date(); 64 | await this.#client.createNamespacedEvent({ 65 | namespace: ref.namespace, 66 | body: { 67 | apiVersion: 'v1', 68 | kind: 'Event', 69 | metadata: { 70 | name: ref.name + process.hrtime().join(''), 71 | namespace: ref.namespace, 72 | }, 73 | action: 'Added', 74 | count: 1, 75 | eventTime: new V1MicroTime(now.getTime()), 76 | firstTimestamp: now, 77 | lastTimestamp: now, 78 | involvedObject: ref, 79 | message, 80 | reason, 81 | reportingComponent: this.#source.host, 82 | reportingInstance: this.#source.component, 83 | source: this.#source, 84 | type: eventType, 85 | }, 86 | }).catch(() => {}); 87 | } 88 | } 89 | 90 | /** 91 | * getReference() returns a reference to the provided object. 92 | * @param {KubernetesObject} object - Kubernetes object. 93 | * @returns {V1ObjectReference} 94 | */ 95 | function getReference(object) { 96 | const gvk = GroupVersionKind.fromKubernetesObject(object); 97 | return { 98 | apiVersion: gvk.toAPIVersion(), 99 | kind: gvk.kind, 100 | name: object.metadata?.name, 101 | namespace: object.metadata?.namespace, 102 | resourceVersion: object.metadata?.resourceVersion, 103 | uid: object.metadata?.uid, 104 | }; 105 | } 106 | 107 | export default { 108 | EventRecorder, 109 | }; 110 | -------------------------------------------------------------------------------- /controller-runtime/lib/source.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('@kubernetes/client-node').KubernetesObject} KubernetesObject 3 | */ 4 | /** 5 | * Source provides event streams to hook up to Controllers. 6 | */ 7 | export class Source { 8 | /** 9 | * Construct a Source. 10 | * @param {KubeConfig} kubeconfig - Kubeconfig to use. 11 | * @param {KubernetesObjectApi} client - Kubernetes client to use. 12 | * @param {string} kind - Resource kind to watch. 13 | * @param {string} [apiVersion] - API version of resource to watch. 14 | */ 15 | constructor(kubeconfig: KubeConfig, client: KubernetesObjectApi, kind: string, apiVersion?: string); 16 | kubeconfig: KubeConfig; 17 | client: KubernetesObjectApi; 18 | kind: string; 19 | apiVersion: string; 20 | informer: import("@kubernetes/client-node").Informer; 21 | /** 22 | * start() causes the Source to start watching for and reporting events. 23 | * @param {Context} context - Context to use. 24 | * @param {Queue} queue - Queue to insert observed events into. 25 | * @returns {Promise} 26 | */ 27 | start(context: Context, queue: Queue): Promise; 28 | /** 29 | * A boolean indicating if the source was started. 30 | * @type {boolean} 31 | */ 32 | get started(): boolean; 33 | /** 34 | * stop() causes the Source to stop watching for and reporting events. If the 35 | * Source was already stopped, this is a no-op. 36 | * @returns {Promise} 37 | */ 38 | stop(): Promise; 39 | #private; 40 | } 41 | declare namespace _default { 42 | export { Source }; 43 | } 44 | export default _default; 45 | export type KubernetesObject = import("@kubernetes/client-node").KubernetesObject; 46 | import { KubeConfig } from '@kubernetes/client-node'; 47 | import { KubernetesObjectApi } from '@kubernetes/client-node'; 48 | import { Context } from './context.js'; 49 | import { Queue } from './queue.js'; 50 | import { Request } from './reconcile.js'; 51 | -------------------------------------------------------------------------------- /controller-runtime/lib/source.js: -------------------------------------------------------------------------------- 1 | import { 2 | KubeConfig, 3 | KubernetesObjectApi, 4 | makeInformer, 5 | } from '@kubernetes/client-node'; 6 | import { Context } from './context.js'; 7 | import { Queue } from './queue.js'; 8 | import { Request } from './reconcile.js'; 9 | 10 | /** 11 | * @typedef {import('@kubernetes/client-node').KubernetesObject} KubernetesObject 12 | */ 13 | 14 | /** 15 | * Source provides event streams to hook up to Controllers. 16 | */ 17 | export class Source { 18 | /** @type boolean */ 19 | #started; 20 | 21 | /** 22 | * Construct a Source. 23 | * @param {KubeConfig} kubeconfig - Kubeconfig to use. 24 | * @param {KubernetesObjectApi} client - Kubernetes client to use. 25 | * @param {string} kind - Resource kind to watch. 26 | * @param {string} [apiVersion] - API version of resource to watch. 27 | */ 28 | constructor(kubeconfig, client, kind, apiVersion = 'v1') { 29 | if (!(kubeconfig instanceof KubeConfig)) { 30 | throw new TypeError('kubeconfig must be a KubeConfig instance'); 31 | } 32 | 33 | if (!(client instanceof KubernetesObjectApi)) { 34 | throw new TypeError('client must be a KubernetesObjectApi instance'); 35 | } 36 | 37 | if (typeof kind !== 'string') { 38 | throw new TypeError('kind must be a string'); 39 | } 40 | 41 | if (typeof apiVersion !== 'string') { 42 | throw new TypeError('apiVersion must be a string'); 43 | } 44 | 45 | this.kubeconfig = kubeconfig; 46 | this.client = client; 47 | this.kind = kind; 48 | this.apiVersion = apiVersion; 49 | this.informer = null; 50 | this.#started = false; 51 | } 52 | 53 | /** 54 | * start() causes the Source to start watching for and reporting events. 55 | * @param {Context} context - Context to use. 56 | * @param {Queue} queue - Queue to insert observed events into. 57 | * @returns {Promise} 58 | */ 59 | async start(context, queue) { 60 | if (!(context instanceof Context)) { 61 | throw new TypeError('context must be a Context instance'); 62 | } 63 | 64 | if (!(queue instanceof Queue)) { 65 | throw new TypeError('queue must be a Queue instance'); 66 | } 67 | 68 | if (this.#started) { 69 | throw new Error('source already started'); 70 | } 71 | 72 | let apiVersion = this.apiVersion; 73 | 74 | if (apiVersion !== 'v1' && apiVersion.split('/').length === 1) { 75 | // @ts-ignore 76 | const res = await this.client.getAPIVersions(); 77 | const api = res?.body?.groups?.find((group) => { 78 | return group.name === apiVersion; 79 | }); 80 | 81 | if (api === undefined) { 82 | throw new Error(`unknown API '${apiVersion}'`); 83 | } 84 | 85 | apiVersion = `${apiVersion}/${api.preferredVersion.version}`; 86 | } 87 | 88 | // @ts-ignore 89 | const resource = await this.client.resource(apiVersion, this.kind); 90 | 91 | if (resource === undefined) { 92 | throw new Error(`unknown kind '${this.kind}' in API '${apiVersion}'`); 93 | } 94 | 95 | let informerPath; 96 | 97 | if (apiVersion.includes('/')) { 98 | informerPath = `/apis/${apiVersion}/${resource.name}`; 99 | } else { 100 | informerPath = `/api/${apiVersion}/${resource.name}`; 101 | } 102 | 103 | this.informer = makeInformer(this.kubeconfig, informerPath, () => { 104 | return this.client.list(apiVersion, this.kind); 105 | }); 106 | 107 | this.informer.on('error', (err) => { 108 | // @ts-ignore 109 | if (err?.code !== 'ECONNRESET') { 110 | throw err; 111 | } 112 | 113 | setImmediate(() => { 114 | this.informer.start(); 115 | }); 116 | }); 117 | 118 | this.informer.on('add', (resource) => { 119 | queue.enqueue(k8sObjectToRequest(resource)); 120 | }); 121 | 122 | this.informer.on('update', (resource) => { 123 | queue.enqueue(k8sObjectToRequest(resource)); 124 | }); 125 | 126 | this.informer.on('delete', (resource) => { 127 | queue.enqueue(k8sObjectToRequest(resource)); 128 | }); 129 | 130 | await this.informer.start(); 131 | 132 | context.signal.addEventListener('abort', () => { 133 | this.stop(); 134 | }); 135 | 136 | this.#started = true; 137 | } 138 | 139 | /** 140 | * A boolean indicating if the source was started. 141 | * @type {boolean} 142 | */ 143 | get started() { 144 | return this.#started; 145 | } 146 | 147 | /** 148 | * stop() causes the Source to stop watching for and reporting events. If the 149 | * Source was already stopped, this is a no-op. 150 | * @returns {Promise} 151 | */ 152 | async stop() { 153 | if (!this.#started) { 154 | return; 155 | } 156 | 157 | await this.informer.stop(); 158 | this.#started = false; 159 | } 160 | } 161 | 162 | /** 163 | * k8sObjectToRequest() creates a Request based on a Kubernetes object. 164 | * @param {KubernetesObject} obj - Kubernetes object. 165 | * @returns {Request} 166 | */ 167 | function k8sObjectToRequest(obj) { 168 | const { name, namespace } = obj.metadata; 169 | 170 | return new Request(name, namespace); 171 | } 172 | 173 | export default { Source }; 174 | -------------------------------------------------------------------------------- /controller-runtime/lib/util.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} PromiseWithResolvers 3 | * @property {Promise} promise 4 | * @property {function} resolve 5 | * @property {function} reject 6 | */ 7 | /** 8 | * withResolvers() works like Promise.withResolvers(). 9 | * @returns {PromiseWithResolvers} 10 | */ 11 | export function withResolvers(): PromiseWithResolvers; 12 | declare namespace _default { 13 | export { withResolvers }; 14 | } 15 | export default _default; 16 | export type PromiseWithResolvers = { 17 | promise: Promise; 18 | resolve: Function; 19 | reject: Function; 20 | }; 21 | -------------------------------------------------------------------------------- /controller-runtime/lib/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} PromiseWithResolvers 3 | * @property {Promise} promise 4 | * @property {function} resolve 5 | * @property {function} reject 6 | */ 7 | 8 | /** 9 | * withResolvers() works like Promise.withResolvers(). 10 | * @returns {PromiseWithResolvers} 11 | */ 12 | export function withResolvers() { 13 | let resolve; 14 | let reject; 15 | const promise = new Promise((res, rej) => { 16 | resolve = res; 17 | reject = rej; 18 | }); 19 | return { promise, resolve, reject }; 20 | } 21 | 22 | export default { 23 | withResolvers, 24 | }; 25 | -------------------------------------------------------------------------------- /controller-runtime/lib/webhook/server.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} ServerOptions 3 | * @property {string} [certDir] The directory that contains the server key and certificate. 4 | * @property {string} [certName] The server certificate name. Defaults to tls.crt. 5 | * @property {boolean} [insecure] If true, the server uses HTTP instead of HTTPS. 6 | * @property {string} [keyName] The server key name. Defaults to tls.key. 7 | * @property {number} [port] The port number that the server will bind to. 8 | */ 9 | /** 10 | * Server is a generic Kubernetes webhook server. 11 | */ 12 | export class Server { 13 | /** 14 | * Creates a new Server instance. 15 | * @param {ServerOptions} [options] Options used to construct instance. 16 | */ 17 | constructor(options?: ServerOptions); 18 | /** 19 | * inject() creates a simulated request in the server. 20 | * @param {Object} settings Simulated request configuration. 21 | * @returns {Promise} 22 | */ 23 | inject(settings: any): Promise; 24 | /** 25 | * register() marks the given webhook as being served at the given path. 26 | * @param {string} path The path to serve the webhook from. 27 | * @param {function} hook The webhook to serve. 28 | */ 29 | register(path: string, hook: Function): void; 30 | /** 31 | * start() runs the server. 32 | * @param {Context} context The context object. 33 | * @returns {Promise} 34 | */ 35 | start(context: Context): Promise; 36 | /** 37 | * A boolean indicating if the manager was started. 38 | * @type {boolean} 39 | */ 40 | get started(): boolean; 41 | /** 42 | * stop() causes the server to stop listening for connections. If the server 43 | * was already stopped, this is a no-op. 44 | * @returns {Promise} 45 | */ 46 | stop(): Promise; 47 | #private; 48 | } 49 | declare namespace _default { 50 | export { Server }; 51 | } 52 | export default _default; 53 | export type RequestListener = import("node:http").RequestListener; 54 | export type IncomingMessage = import("node:http").IncomingMessage; 55 | export type InsecureServer = import("node:http").Server; 56 | export type ServerResponse = import("node:http").ServerResponse; 57 | export type SecureServer = import("node:https").Server; 58 | export type ServerOptions = { 59 | /** 60 | * The directory that contains the server key and certificate. 61 | */ 62 | certDir?: string; 63 | /** 64 | * The server certificate name. Defaults to tls.crt. 65 | */ 66 | certName?: string; 67 | /** 68 | * If true, the server uses HTTP instead of HTTPS. 69 | */ 70 | insecure?: boolean; 71 | /** 72 | * The server key name. Defaults to tls.key. 73 | */ 74 | keyName?: string; 75 | /** 76 | * The port number that the server will bind to. 77 | */ 78 | port?: number; 79 | }; 80 | import { Context } from '../context.js'; 81 | -------------------------------------------------------------------------------- /controller-runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kubenode/controller-runtime", 3 | "version": "0.4.2", 4 | "description": "Kubernetes controller runtime", 5 | "license": "MIT", 6 | "author": "Colin J. Ihrig (http://www.cjihrig.com/)", 7 | "main": "lib/index.js", 8 | "type": "module", 9 | "scripts": { 10 | "//": "The linter is not happy about the move to ESM so it has been removed from the pretest script.", 11 | "lint": "belly-button -f", 12 | "pretest": "tsc --noEmit", 13 | "test": "node --test --experimental-test-coverage", 14 | "types": "node scripts/clean-types.js && tsc" 15 | }, 16 | "dependencies": { 17 | "@kubernetes/client-node": "^1.2.0" 18 | }, 19 | "devDependencies": { 20 | "belly-button": "^8.0.0", 21 | "typescript": "^5.6.2" 22 | }, 23 | "homepage": "https://github.com/cjihrig/kubenode#readme", 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/cjihrig/kubenode.git" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/cjihrig/kubenode/issues" 30 | }, 31 | "keywords": [ 32 | "kubernetes", 33 | "k8s", 34 | "controller", 35 | "runtime" 36 | ], 37 | "directories": { 38 | "lib": "lib", 39 | "test": "test" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /controller-runtime/scripts/clean-types.js: -------------------------------------------------------------------------------- 1 | import { readdirSync, rmSync } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | const root = join(process.cwd(), 'lib'); 4 | 5 | cleanDir(root); 6 | 7 | function cleanDir(dir) { 8 | const entries = readdirSync(dir, { withFileTypes: true }); 9 | 10 | for (let i = 0; i < entries.length; ++i) { 11 | const entry = entries[i]; 12 | const path = join(entry.parentPath, entry.name); 13 | 14 | if (entry.isDirectory()) { 15 | cleanDir(path); 16 | } else if (path.endsWith('.d.ts')) { 17 | rmSync(path, { force: true }); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /controller-runtime/test/builder.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { suite, test } from 'node:test'; 3 | import { Builder } from '../lib/builder.js'; 4 | import { Controller } from '../lib/controller.js'; 5 | import { Manager } from '../lib/manager.js'; 6 | import { Reconciler } from '../lib/reconcile.js'; 7 | import { getManagerOptions } from './test-utils.js'; 8 | 9 | suite('Builder', () => { 10 | suite('Builder() constructor', () => { 11 | test('successfully constructs a Builder instance', () => { 12 | const manager = new Manager(getManagerOptions()); 13 | const builder = new Builder(manager); 14 | 15 | assert.strictEqual(builder instanceof Builder, true); 16 | }); 17 | 18 | test('throws if a Manager instance is not provided', () => { 19 | assert.throws(() => { 20 | new Builder({}); 21 | }, /TypeError: manager must be a Manager instance/); 22 | }); 23 | }); 24 | 25 | suite('Builder.prototype.build()', () => { 26 | test('successfully builds after calling named()', () => { 27 | const manager = new Manager(getManagerOptions()); 28 | const builder = new Builder(manager); 29 | const reconciler = new Reconciler(); 30 | 31 | builder.named('foobar'); 32 | const controller = builder.build(reconciler); 33 | assert.strictEqual(controller instanceof Controller, true); 34 | assert.strictEqual(controller.name, 'foobar'); 35 | }); 36 | 37 | test('successfully builds after calling for()', () => { 38 | const manager = new Manager(getManagerOptions()); 39 | const builder = new Builder(manager); 40 | const reconciler = new Reconciler(); 41 | 42 | builder.for('Foo', 'bar.com/v1'); 43 | const controller = builder.build(reconciler); 44 | assert.strictEqual(controller instanceof Controller, true); 45 | assert.strictEqual(controller.name, 'foo'); 46 | }); 47 | 48 | test('throws if a Reconciler instance is not provided', () => { 49 | const manager = new Manager(getManagerOptions()); 50 | const builder = new Builder(manager); 51 | 52 | assert.throws(() => { 53 | builder.build({}); 54 | }, /TypeError: reconciler must be a Reconciler instance/); 55 | }); 56 | 57 | test('throws if no controller name can be determined', () => { 58 | const manager = new Manager(getManagerOptions()); 59 | const builder = new Builder(manager); 60 | const reconciler = new Reconciler(); 61 | 62 | assert.throws(() => { 63 | builder.build(reconciler); 64 | }, /controller has no name\. for\(\) or named\(\) must be called/); 65 | }); 66 | }); 67 | 68 | suite('Builder.prototype.complete()', () => { 69 | test('successfully builds', (t) => { 70 | const manager = new Manager(getManagerOptions()); 71 | const builder = new Builder(manager); 72 | const reconciler = new Reconciler(); 73 | 74 | t.mock.method(builder, 'build'); 75 | builder.named('foobar'); 76 | assert.strictEqual(builder.build.mock.calls.length, 0); 77 | assert.strictEqual(builder.complete(reconciler), undefined); 78 | assert.strictEqual(builder.build.mock.calls.length, 1); 79 | assert.deepStrictEqual(builder.build.mock.calls[0].arguments, [ 80 | reconciler 81 | ]); 82 | }); 83 | }); 84 | 85 | suite('Builder.prototype.for()', () => { 86 | test('can only be called once', () => { 87 | const manager = new Manager(getManagerOptions()); 88 | const builder = new Builder(manager); 89 | 90 | builder.for('Foo', 'bar.com/v1'); 91 | assert.throws(() => { 92 | builder.for('Foo', 'bar.com/v1'); 93 | }, /for\(\) can only be called once/); 94 | }); 95 | }); 96 | 97 | suite('Builder.prototype.owns()', () => { 98 | test('is currently unimplemented', () => { 99 | const manager = new Manager(getManagerOptions()); 100 | const builder = new Builder(manager); 101 | 102 | assert.throws(() => { 103 | builder.owns(); 104 | }, /unimplemented/); 105 | }); 106 | }); 107 | 108 | suite('Builder.prototype.watches()', () => { 109 | test('is currently unimplemented', () => { 110 | const manager = new Manager(getManagerOptions()); 111 | const builder = new Builder(manager); 112 | 113 | assert.throws(() => { 114 | builder.watches(); 115 | }, /unimplemented/); 116 | }); 117 | }); 118 | 119 | suite('Builder.controllerManagedBy()', () => { 120 | test('creates a Builder', () => { 121 | const manager = new Manager(getManagerOptions()); 122 | const builder = Builder.controllerManagedBy(manager); 123 | 124 | assert.strictEqual(builder instanceof Builder, true); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /controller-runtime/test/context.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { suite, test } from 'node:test'; 3 | import { Context } from '../lib/index.js'; 4 | import { ReconcileContext } from '../lib/context.js'; 5 | 6 | suite('Context', () => { 7 | suite('Context() constructor', () => { 8 | test('cannot be constructed directly', () => { 9 | assert.throws(() => { 10 | new Context(); 11 | }, /Error: illegal constructor/); 12 | }); 13 | }); 14 | 15 | suite('Context.create()', () => { 16 | test('creates a new context instance', () => { 17 | const ctx = Context.create(); 18 | 19 | assert.strictEqual(ctx instanceof Context, true); 20 | assert.strictEqual(ctx.done instanceof Promise, true); 21 | assert.strictEqual(ctx.signal instanceof AbortSignal, true); 22 | assert.strictEqual(ctx.values instanceof Map, true); 23 | }); 24 | }); 25 | 26 | suite('Context.prototype.child()', () => { 27 | test('creates a child context', () => { 28 | const parent = Context.create(); 29 | const child = parent.child(); 30 | 31 | assert.strictEqual(child instanceof Context, true); 32 | assert.strictEqual(child.done instanceof Promise, true); 33 | assert.strictEqual(child.signal instanceof AbortSignal, true); 34 | assert.strictEqual(child.values instanceof Map, true); 35 | assert.notStrictEqual(child.done, parent.done); 36 | assert.notStrictEqual(child.signal, parent.signal); 37 | assert.notStrictEqual(child.values, parent.values); 38 | }); 39 | }); 40 | 41 | suite('Context.prototype.cancel()', () => { 42 | test('aborts the signal', async () => { 43 | const ctx = Context.create(); 44 | 45 | assert.strictEqual(ctx.signal.aborted, false); 46 | ctx.cancel(); 47 | assert.strictEqual(ctx.signal.aborted, true); 48 | await assert.rejects(ctx.done); 49 | }); 50 | 51 | test('parent aborts the signal of a child context', async () => { 52 | const parent = Context.create(); 53 | const child = parent.child(); 54 | 55 | assert.strictEqual(child.signal.aborted, false); 56 | parent.cancel(); 57 | assert.strictEqual(child.signal.aborted, true); 58 | await assert.rejects(parent.done); 59 | await assert.rejects(child.done); 60 | }); 61 | 62 | test('child does not abort the signal of the parent context', async () => { 63 | const parent = Context.create(); 64 | const child = parent.child(); 65 | 66 | assert.strictEqual(child.signal.aborted, false); 67 | assert.strictEqual(parent.signal.aborted, false); 68 | child.cancel(); 69 | assert.strictEqual(child.signal.aborted, true); 70 | assert.strictEqual(parent.signal.aborted, false); 71 | await assert.rejects(child.done); 72 | }); 73 | 74 | test('does not cause unhandledRejections', () => { 75 | const ctx = Context.create(); 76 | 77 | assert.strictEqual(ctx.signal.aborted, false); 78 | ctx.cancel(); 79 | assert.strictEqual(ctx.signal.aborted, true); 80 | // At this point, the test would complete and an unhandledRejection would 81 | // occur if we weren't handling this properly in the context. 82 | }); 83 | }); 84 | }); 85 | 86 | suite('ReconcileContext', () => { 87 | suite('ReconcileContext() constructor', () => { 88 | test('cannot be constructed directly', () => { 89 | assert.throws(() => { 90 | new ReconcileContext(); 91 | }, /Error: illegal constructor/); 92 | }); 93 | }); 94 | 95 | suite('ReconcileContext.fromContext()', () => { 96 | test('creates a new ReconcileContext instance', () => { 97 | const parent = Context.create(); 98 | const rc = ReconcileContext.fromContext(parent, 'foobar'); 99 | 100 | assert.strictEqual(rc instanceof Context, true); 101 | assert.strictEqual(rc instanceof ReconcileContext, true); 102 | assert.strictEqual(rc.done instanceof Promise, true); 103 | assert.strictEqual(rc.signal instanceof AbortSignal, true); 104 | assert.strictEqual(rc.values instanceof Map, true); 105 | assert.strictEqual(rc.reconcileID, 'foobar'); 106 | }); 107 | 108 | test('throws if context is not a Context instance', () => { 109 | assert.throws(() => { 110 | ReconcileContext.fromContext({}, 'foobar'); 111 | }, /TypeError: context must be a Context instance/); 112 | }); 113 | 114 | test('throws if reconcileID is not a string', () => { 115 | assert.throws(() => { 116 | ReconcileContext.fromContext(Context.create(), 5); 117 | }, /TypeError: reconcileID must be a string/); 118 | }); 119 | }); 120 | 121 | suite('ReconcileContext.prototype.child()', () => { 122 | test('creates a child ReconcileContext', () => { 123 | const parent = Context.create(); 124 | const rcParent = ReconcileContext.fromContext(parent, 'foobar'); 125 | const rcChild = rcParent.child(); 126 | 127 | assert.strictEqual(rcChild instanceof Context, true); 128 | assert.strictEqual(rcChild.done instanceof Promise, true); 129 | assert.strictEqual(rcChild.signal instanceof AbortSignal, true); 130 | assert.strictEqual(rcChild.values instanceof Map, true); 131 | assert.strictEqual(rcChild.reconcileID, rcParent.reconcileID); 132 | assert.notStrictEqual(rcChild.done, parent.done); 133 | assert.notStrictEqual(rcChild.signal, parent.signal); 134 | assert.notStrictEqual(rcChild.values, parent.values); 135 | }); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /controller-runtime/test/interop.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { test } from 'node:test'; 3 | import defaultExport, { 4 | k8s, 5 | apimachinery, 6 | controllerutil, 7 | Manager, 8 | newControllerManagedBy, 9 | Reconciler, 10 | Request, 11 | Result, 12 | Source, 13 | TerminalError, 14 | webhook 15 | } from '../lib/index.js'; 16 | 17 | test('esm exports are configured properly', () => { 18 | assert.strictEqual(defaultExport.k8s, k8s); 19 | assert.strictEqual(defaultExport.apimachinery, apimachinery); 20 | assert.strictEqual(defaultExport.controllerutil, controllerutil); 21 | assert.strictEqual(defaultExport.Manager, Manager); 22 | assert.strictEqual( 23 | defaultExport.newControllerManagedBy, newControllerManagedBy 24 | ); 25 | assert.strictEqual(defaultExport.Reconciler, Reconciler); 26 | assert.strictEqual(defaultExport.Request, Request); 27 | assert.strictEqual(defaultExport.Result, Result); 28 | assert.strictEqual(defaultExport.Source, Source); 29 | assert.strictEqual(defaultExport.TerminalError, TerminalError); 30 | assert.strictEqual(defaultExport.webhook, webhook); 31 | }); 32 | -------------------------------------------------------------------------------- /controller-runtime/test/leaderelection/test-utils.js: -------------------------------------------------------------------------------- 1 | import { LeaseLock } from '../../lib/leaderelection/leaselock.js'; 2 | 3 | export function createNotFoundError() { 4 | const err = new Error('mock not found'); 5 | err.body = { code: 404 }; 6 | return err; 7 | } 8 | 9 | export function getClient(mock) { 10 | const store = new Map(); 11 | 12 | return { 13 | store, 14 | createNamespacedLease: mock.fn((o) => { 15 | const key = `${o.body.metadata.namespace}/${o.body.metadata.name}`; 16 | const val = o.body; 17 | 18 | store.set(key, val); 19 | return val; 20 | }), 21 | readNamespacedLease: mock.fn((o) => { 22 | const key = `${o.namespace}/${o.name}`; 23 | const val = store.get(key); 24 | 25 | if (val === undefined) { 26 | const err = createNotFoundError(); 27 | throw err; 28 | } 29 | 30 | return val; 31 | }), 32 | replaceNamespacedLease: mock.fn((o) => { 33 | const key = `${o.body.metadata.namespace}/${o.body.metadata.name}`; 34 | const val = o.body; 35 | 36 | store.set(key, val); 37 | return val; 38 | }), 39 | }; 40 | } 41 | 42 | export function getLease() { 43 | // V1MicroTime extends from JS Date. 44 | const acquireTime = new Date(); 45 | const renewTime = new Date(acquireTime.getTime() + (2 * 60_000)); 46 | 47 | return { 48 | apiVersion: 'coordination.k8s.io/v1', 49 | kind: 'Lease', 50 | metadata: { 51 | name: 'test-name', 52 | namespace: 'test-ns', 53 | }, 54 | spec: { 55 | holderIdentity: 'bar', 56 | leaseDurationSeconds: 13, 57 | acquireTime, 58 | renewTime, 59 | leaseTransitions: 99, 60 | } 61 | }; 62 | } 63 | 64 | export function getLeaseLock(mock) { 65 | const meta = getMetaObject(); 66 | const client = getClient(mock); 67 | const lockConfig = getLockConfig(mock); 68 | 69 | return new LeaseLock(meta, client, lockConfig); 70 | } 71 | 72 | export function getLockConfig(mock) { 73 | return { 74 | identity: 'test-id', 75 | eventRecorder: { 76 | event: mock.fn(), 77 | } 78 | }; 79 | } 80 | 81 | export function getMetaObject() { 82 | return { name: 'test-name', namespace: 'test-ns' }; 83 | } 84 | 85 | export function getRecord() { 86 | // V1MicroTime extends from JS Date. 87 | const acquireTime = new Date(); 88 | const renewTime = new Date(acquireTime.getTime() + (2 * 60_000)); 89 | 90 | return { 91 | holderIdentity: 'foo', 92 | leaseDurationSeconds: 15, 93 | acquireTime, 94 | renewTime, 95 | leaderTransitions: 100, 96 | strategy: undefined, 97 | preferredHolder: undefined, 98 | }; 99 | } 100 | 101 | export default { 102 | createNotFoundError, 103 | getClient, 104 | getLease, 105 | getLeaseLock, 106 | getLockConfig, 107 | getMetaObject, 108 | getRecord, 109 | } 110 | -------------------------------------------------------------------------------- /controller-runtime/test/reconcile.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { suite, test } from 'node:test'; 3 | import { 4 | Reconciler, 5 | Request, 6 | Result, 7 | TerminalError, 8 | } from '../lib/index.js'; 9 | 10 | class TestReconciler extends Reconciler { 11 | async reconcile(context, request, ...rest) { 12 | assert.strictEqual(rest.length, 0); 13 | return [context, request]; 14 | } 15 | } 16 | 17 | suite('Reconciler', () => { 18 | suite('Reconciler.prototype.reconcile()', () => { 19 | test('throws if reconcile() is not overridden', async () => { 20 | const r = new Reconciler(); 21 | 22 | await assert.rejects(async () => { 23 | await r.reconcile(); 24 | }, /unimplemented reconcile\(\)/); 25 | }); 26 | 27 | test('is meant to be overridden', async () => { 28 | const r = new TestReconciler(); 29 | 30 | assert.strictEqual(r instanceof Reconciler, true); 31 | assert.deepStrictEqual(await r.reconcile('ctx', 'req'), ['ctx', 'req']); 32 | }); 33 | }); 34 | }); 35 | 36 | suite('Request', () => { 37 | suite('Request() constructor', () => { 38 | test('creates a Request instance', () => { 39 | const req = new Request('test-name', 'test-namespace'); 40 | 41 | assert.strictEqual(req.name, 'test-name'); 42 | assert.strictEqual(req.namespace, 'test-namespace'); 43 | }); 44 | 45 | test('namespace defaults to empty string', () => { 46 | const req = new Request('test-name'); 47 | 48 | assert.strictEqual(req.name, 'test-name'); 49 | assert.strictEqual(req.namespace, ''); 50 | }); 51 | }); 52 | 53 | suite('Request.prototype.toString()', () => { 54 | test('converts the Request to a string', () => { 55 | const req = new Request('test-name', 'test-namespace'); 56 | 57 | assert.strictEqual(req.toString(), 'test-namespace/test-name'); 58 | }); 59 | }); 60 | }); 61 | 62 | suite('Result', () => { 63 | suite('Result() constructor', () => { 64 | test('creates a Result with no arguments', () => { 65 | const res = new Result(); 66 | 67 | assert.strictEqual(res.requeue, false); 68 | assert.strictEqual(res.requeueAfter, 0); 69 | }); 70 | 71 | test('creates a Result with a number', () => { 72 | const res = new Result(5); 73 | 74 | assert.strictEqual(res.requeue, true); 75 | assert.strictEqual(res.requeueAfter, 5); 76 | }); 77 | 78 | test('creates a Result with a boolean', () => { 79 | const res1 = new Result(true); 80 | const res2 = new Result(false); 81 | 82 | assert.strictEqual(res1.requeue, true); 83 | assert.strictEqual(res1.requeueAfter, 0); 84 | assert.strictEqual(res2.requeue, false); 85 | assert.strictEqual(res2.requeueAfter, 0); 86 | }); 87 | }); 88 | }); 89 | 90 | suite('TerminalError', () => { 91 | suite('TerminalError() constructor', () => { 92 | test('creates a TerminalError instance', () => { 93 | const cause = new Error('boom'); 94 | const err = new TerminalError(cause); 95 | 96 | assert.strictEqual(err instanceof TerminalError, true); 97 | assert.strictEqual(err instanceof Error, true); 98 | assert.strictEqual(err.message, 'terminal error'); 99 | assert.strictEqual(err.name, 'TerminalError'); 100 | assert.strictEqual(err.cause, cause); 101 | }); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /controller-runtime/test/record/recorder.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { suite, test } from 'node:test'; 3 | import { V1MicroTime } from '@kubernetes/client-node'; 4 | import { EventRecorder } from '../../lib/record/recorder.js'; 5 | 6 | function getEventSource() { 7 | return { 8 | host: 'test-host', 9 | component: 'test-component', 10 | }; 11 | } 12 | 13 | function getClient(mock) { 14 | const store = new Map(); 15 | 16 | return { 17 | store, 18 | createNamespacedEvent: mock.fn(async (o) => { 19 | const key = `${o.body.metadata.namespace}/${o.body.metadata.name}`; 20 | const val = o.body; 21 | 22 | store.set(key, val); 23 | return val; 24 | }), 25 | }; 26 | } 27 | 28 | suite('EventRecorder', () => { 29 | suite('EventRecorder() constructor', () => { 30 | test('constructs an EventRecorder instance', (t) => { 31 | const rec = new EventRecorder(getEventSource(), getClient(t.mock)); 32 | assert.strictEqual(rec instanceof EventRecorder, true); 33 | }); 34 | }); 35 | 36 | suite('EventRecorder.prototype.event()', () => { 37 | test('creates an event based on the arguments', async (t) => { 38 | const source = getEventSource(); 39 | const client = getClient(t.mock); 40 | const rec = new EventRecorder(source, client); 41 | const subject = { 42 | apiVersion: 'foo.bar.com/v1', 43 | kind: 'Blah', 44 | metadata: { 45 | name: 'test-name', 46 | namespace: 'test-namespace', 47 | resourceVersion: '1360334', 48 | uid: '01defbd3-4900-4665-bc5c-9b294988b326', 49 | }, 50 | }; 51 | const r = await rec.event(subject, 'Warning', 'Custom', 'hello there'); 52 | 53 | assert.strictEqual(r, undefined); 54 | assert.strictEqual(client.createNamespacedEvent.mock.calls.length, 1); 55 | const arg = client.createNamespacedEvent.mock.calls[0].arguments[0]; 56 | assert.strictEqual(arg.namespace, subject.metadata.namespace); 57 | assert.strictEqual(arg.body.apiVersion, 'v1'); 58 | assert.strictEqual(arg.body.kind, 'Event'); 59 | assert.match(arg.body.metadata.name, /test-name\d+/); 60 | assert.strictEqual( 61 | arg.body.metadata.namespace, subject.metadata.namespace 62 | ); 63 | assert.strictEqual(arg.body.action, 'Added'); 64 | assert.strictEqual(arg.body.count, 1); 65 | assert.strictEqual(arg.body.eventTime instanceof V1MicroTime, true); 66 | assert.strictEqual(arg.body.firstTimestamp instanceof Date, true); 67 | assert.strictEqual(arg.body.firstTimestamp, arg.body.lastTimestamp); 68 | assert.strictEqual(arg.body.message, 'hello there'); 69 | assert.strictEqual(arg.body.reason, 'Custom'); 70 | assert.strictEqual(arg.body.reportingComponent, source.host); 71 | assert.strictEqual(arg.body.reportingInstance, source.component); 72 | assert.strictEqual(arg.body.source, source); 73 | assert.strictEqual(arg.body.type, 'Warning'); 74 | assert.deepStrictEqual(arg.body.involvedObject, { 75 | apiVersion: subject.apiVersion, 76 | kind: subject.kind, 77 | name: subject.metadata.name, 78 | namespace: subject.metadata.namespace, 79 | resourceVersion: subject.metadata.resourceVersion, 80 | uid: subject.metadata.uid, 81 | }); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /controller-runtime/test/source.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { suite, test } from 'node:test'; 3 | import { Context } from '../lib/context.js'; 4 | import { Queue } from '../lib/queue.js'; 5 | import { Source } from '../lib/source.js'; 6 | import { getSourceOptions } from './test-utils.js'; 7 | 8 | suite('Source', () => { 9 | suite('Source() constructor', () => { 10 | test('successfully constructs a Source instance', () => { 11 | const o = getSourceOptions(); 12 | const source = new Source(o.kubeconfig, o.client, o.kind, o.apiVersion); 13 | 14 | assert.strictEqual(source instanceof Source, true); 15 | assert.strictEqual(source.kubeconfig, o.kubeconfig); 16 | assert.strictEqual(source.client, o.client); 17 | assert.strictEqual(source.kind, o.kind); 18 | assert.strictEqual(source.apiVersion, o.apiVersion); 19 | assert.strictEqual(source.started, false); 20 | }); 21 | 22 | test('kubeconfig argument must be a KubeConfig instance', () => { 23 | const o = getSourceOptions(); 24 | o.kubeconfig = null; 25 | 26 | assert.throws(() => { 27 | new Source(o.kubeconfig, o.client, o.kind, o.apiVersion); 28 | }, /TypeError: kubeconfig must be a KubeConfig instance/); 29 | }); 30 | 31 | test('client argument must be a KubernetesObjectApi instance', () => { 32 | const o = getSourceOptions(); 33 | o.client = null; 34 | 35 | assert.throws(() => { 36 | new Source(o.kubeconfig, o.client, o.kind, o.apiVersion); 37 | }, /TypeError: client must be a KubernetesObjectApi instance/); 38 | }); 39 | 40 | test('kind argument must be a string', () => { 41 | const o = getSourceOptions(); 42 | o.kind = null; 43 | 44 | assert.throws(() => { 45 | new Source(o.kubeconfig, o.client, o.kind, o.apiVersion); 46 | }, /TypeError: kind must be a string/); 47 | }); 48 | 49 | test('apiVersion argument must be a string', () => { 50 | const o = getSourceOptions(); 51 | o.apiVersion = null; 52 | 53 | assert.throws(() => { 54 | new Source(o.kubeconfig, o.client, o.kind, o.apiVersion); 55 | }, /TypeError: apiVersion must be a string/); 56 | }); 57 | }); 58 | 59 | suite('Source.prototype.start()', () => { 60 | test('context argument must be a Context instance', async () => { 61 | const o = getSourceOptions(); 62 | const queue = new Queue(); 63 | const source = new Source(o.kubeconfig, o.client, o.kind, o.apiVersion); 64 | 65 | await assert.rejects(async () => { 66 | await source.start(null, queue); 67 | }, /TypeError: context must be a Context instance/); 68 | }); 69 | 70 | test('queue argument must be a Queue instance', async () => { 71 | const o = getSourceOptions(); 72 | const context = Context.create(); 73 | const source = new Source(o.kubeconfig, o.client, o.kind, o.apiVersion); 74 | 75 | await assert.rejects(async () => { 76 | await source.start(context, null); 77 | }, /TypeError: queue must be a Queue instance/); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /controller-runtime/test/test-utils.js: -------------------------------------------------------------------------------- 1 | import { 2 | CoordinationV1Api, 3 | CoreV1Api, 4 | KubeConfig, 5 | KubernetesObjectApi 6 | } from '@kubernetes/client-node'; 7 | 8 | export function getManagerOptions() { 9 | const kubeconfig = new KubeConfig(); 10 | const kcOptions = { 11 | clusters: [{ name: 'cluster', server: 'https://127.0.0.1:51010' }], 12 | users: [{ name: 'user', password: 'password' }], 13 | contexts: [{ name: 'currentContext', cluster: 'cluster', user: 'user' }], 14 | currentContext: 'currentContext', 15 | }; 16 | kubeconfig.loadFromOptions(kcOptions); 17 | 18 | return { 19 | kubeconfig, 20 | client: KubernetesObjectApi.makeApiClient(kubeconfig), 21 | coordinationClient: kubeconfig.makeApiClient(CoordinationV1Api), 22 | coreClient: kubeconfig.makeApiClient(CoreV1Api), 23 | leaderElection: true, 24 | leaderElectionName: 'test-lock', 25 | leaderElectionNamespace: 'test-ns', 26 | leaseDuration: 15_000, 27 | renewDeadline: 10_000, 28 | retryPeriod: 2_000, 29 | }; 30 | } 31 | 32 | export function getSourceOptions() { 33 | const kubeconfig = new KubeConfig(); 34 | const kcOptions = { 35 | clusters: [{ name: 'cluster', server: 'https://127.0.0.1:51010' }], 36 | users: [{ name: 'user', password: 'password' }], 37 | contexts: [{ name: 'currentContext', cluster: 'cluster', user: 'user' }], 38 | currentContext: 'currentContext', 39 | }; 40 | kubeconfig.loadFromOptions(kcOptions); 41 | 42 | return { 43 | kubeconfig, 44 | client: KubernetesObjectApi.makeApiClient(kubeconfig), 45 | kind: 'Foo', 46 | apiVersion: 'bar.com/v1', 47 | }; 48 | } 49 | 50 | export default { 51 | getManagerOptions, 52 | getSourceOptions, 53 | } 54 | -------------------------------------------------------------------------------- /controller-runtime/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "target": "es2022" 10 | }, 11 | "include": ["lib/**/*.js"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /crdgen/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /crdgen/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | -------------------------------------------------------------------------------- /crdgen/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Colin Ihrig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /crdgen/README.md: -------------------------------------------------------------------------------- 1 | # `@kubenode/crdgen` 2 | 3 | Utilities for creating Kubernetes CRDs from TypeScript definitions. 4 | 5 | ## Classes 6 | 7 |
8 |
Model
9 |

Class representing a single custom resource model.

10 |
11 |
12 | 13 | ## Functions 14 | 15 |
16 |
generateModelsFromFiles(filenames)Map.<string, Model>
17 |

Generates models defined in a collection of files.

18 |
19 |
getModelConfigFromComments([jsDocs])ModelCommentConfig
20 |

Extracts a model configuration from JSDoc comments.

21 |
22 |
23 | 24 | ## Typedefs 25 | 26 |
27 |
ModelCommentConfig : object
28 |
29 |
ModelCRD : object
30 |
31 |
32 | 33 | 34 | 35 | ## Model 36 | Class representing a single custom resource model. 37 | 38 | **Kind**: global class 39 | 40 | * [Model](#Model) 41 | * [new Model(program, node, config)](#new_Model_new) 42 | * [.toCRD()](#Model+toCRD) ⇒ [ModelCRD](#ModelCRD) 43 | 44 | 45 | 46 | ### new Model(program, node, config) 47 | Construct a model. 48 | 49 | 50 | | Param | Type | Description | 51 | | --- | --- | --- | 52 | | program | ts.Program | TypeScript API Program. | 53 | | node | ts.TypeAliasDeclaration | TypeScript API TypeAliasDeclaration node. | 54 | | config | [ModelCommentConfig](#ModelCommentConfig) | Model configuration extracted from JSDoc comments. | 55 | 56 | 57 | 58 | ### model.toCRD() ⇒ [ModelCRD](#ModelCRD) 59 | Converts the Model to a CRD object that can be stringified to YAML. 60 | 61 | **Kind**: instance method of [Model](#Model) 62 | **Returns**: [ModelCRD](#ModelCRD) - An object representation of the Model as a Kubernetes CRD. 63 | 64 | 65 | ## generateModelsFromFiles(filenames) ⇒ Map.<string, Model> 66 | Generates models defined in a collection of files. 67 | 68 | **Kind**: global function 69 | 70 | | Param | Type | Description | 71 | | --- | --- | --- | 72 | | filenames | Array.<string> | The files to extract models from. | 73 | 74 | 75 | 76 | ## getModelConfigFromComments([jsDocs]) ⇒ [ModelCommentConfig](#ModelCommentConfig) 77 | Extracts a model configuration from JSDoc comments. 78 | 79 | **Kind**: global function 80 | 81 | | Param | Type | 82 | | --- | --- | 83 | | [jsDocs] | Array.<any> | 84 | 85 | 86 | 87 | ## ModelCommentConfig : object 88 | **Kind**: global typedef 89 | **Properties** 90 | 91 | | Name | Type | 92 | | --- | --- | 93 | | apiVersion | string | 94 | | description | string | 95 | | kind | string | 96 | | isNamespaced | boolean | 97 | | plural | string | 98 | | singular | string | 99 | 100 | 101 | 102 | ## ModelCRD : object 103 | **Kind**: global typedef 104 | **Properties** 105 | 106 | | Name | Type | 107 | | --- | --- | 108 | | apiVersion | string | 109 | | kind | string | 110 | | metadata | object | 111 | | spec | object | 112 | -------------------------------------------------------------------------------- /crdgen/fixtures/book.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @kubenode 3 | * @apiVersion library.io/v1 4 | * @kind Book 5 | * @scope cluster 6 | * @plural books 7 | * @singular book 8 | * @description Schema for the books API 9 | */ 10 | 11 | /** 12 | * Another comment, which kubenode ignores. 13 | */ 14 | type Book = { 15 | /** 16 | * @description apiVersion defines the versioned schema of this representation 17 | * of an object. Servers should convert recognized schemas to the latest 18 | * internal value, and may reject unrecognized values. 19 | */ 20 | apiVersion: string; 21 | 22 | /** 23 | * @description kind is a string value representing the REST resource this 24 | * object represents. 25 | */ 26 | kind: string; 27 | 28 | /** 29 | * @description metadata is a standard Kubernetes object for metadata. 30 | */ 31 | metadata: object; 32 | 33 | /** 34 | * @description spec defines the desired state of Book. 35 | */ 36 | spec: BookSpec; 37 | }; 38 | 39 | type BookSpec = { 40 | title: string; 41 | }; 42 | -------------------------------------------------------------------------------- /crdgen/fixtures/functions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @kubenode 3 | * @apiVersion functions.io/v1 4 | */ 5 | type FunctionMethod = { 6 | prop0: string; 7 | prop1: () => number; 8 | }; 9 | -------------------------------------------------------------------------------- /crdgen/fixtures/supported-types.ts: -------------------------------------------------------------------------------- 1 | type CustomType = { 2 | prop1: boolean; 3 | }; 4 | 5 | type AliasedType = CustomType; 6 | 7 | /** 8 | * @kubenode 9 | * @apiVersion supported.io/v1 10 | */ 11 | type SupportedTypes = { 12 | prop1: string; 13 | prop2: number; 14 | prop3: boolean; 15 | prop4: object; 16 | prop5: string[]; 17 | prop6: 'a-string'; 18 | prop7: true; 19 | prop8: false; 20 | prop9: 100; 21 | prop10: { subprop: string; }; 22 | prop11: CustomType; 23 | prop12: AliasedType; 24 | prop13: CustomType[]; 25 | prop14: AliasedType[]; 26 | prop15: Date; 27 | }; 28 | -------------------------------------------------------------------------------- /crdgen/fixtures/unsupported-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @kubenode 3 | * @apiVersion unsupported.io/v1 4 | */ 5 | type UndefinedPrimitive = { 6 | prop: undefined; 7 | }; 8 | 9 | /** 10 | * @kubenode 11 | * @apiVersion unsupported.io/v1 12 | */ 13 | type NullPrimitive = { 14 | prop: null; 15 | }; 16 | 17 | /** 18 | * @kubenode 19 | * @apiVersion unsupported.io/v1 20 | */ 21 | type SymbolPrimitive = { 22 | prop: symbol; 23 | }; 24 | 25 | /** 26 | * @kubenode 27 | * @apiVersion unsupported.io/v1 28 | */ 29 | type BigIntPrimitive = { 30 | prop: bigint; 31 | }; 32 | 33 | /** 34 | * @kubenode 35 | * @apiVersion unsupported.io/v1 36 | */ 37 | type FunctionObject = { 38 | prop: Function; 39 | }; 40 | 41 | /** 42 | * @kubenode 43 | * @apiVersion unsupported.io/v1 44 | */ 45 | type ArrayOfFunctionObjects = { 46 | prop: Function[]; 47 | }; 48 | -------------------------------------------------------------------------------- /crdgen/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export type ModelCommentConfig = { 2 | apiVersion: string; 3 | description: string; 4 | kind: string; 5 | isNamespaced: boolean; 6 | plural: string; 7 | singular: string; 8 | }; 9 | /** 10 | * @typedef {object} ModelCommentConfig 11 | * @property {string} apiVersion 12 | * @property {string} description 13 | * @property {string} kind 14 | * @property {boolean} isNamespaced 15 | * @property {string} plural 16 | * @property {string} singular 17 | */ 18 | /** 19 | * Generates models defined in a collection of files. 20 | * @param {string[]} filenames - The files to extract models from. 21 | * @returns {Map} 22 | */ 23 | export function generateModelsFromFiles(filenames: string[]): Map; 24 | import { Model } from "./model"; 25 | -------------------------------------------------------------------------------- /crdgen/lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const ts = require('typescript'); 3 | const { Model } = require('./model'); 4 | const kApiVersionTag = 'apiVersion'; 5 | const kDescriptionTag = 'description'; 6 | const kKindTag = 'kind'; 7 | const kModelTag = 'kubenode'; 8 | const kPluralTag = 'plural'; 9 | const kScopeTag = 'scope'; 10 | const kSingularTag = 'singular'; 11 | 12 | /** 13 | * @typedef {object} ModelCommentConfig 14 | * @property {string} apiVersion 15 | * @property {string} description 16 | * @property {string} kind 17 | * @property {boolean} isNamespaced 18 | * @property {string} plural 19 | * @property {string} singular 20 | */ 21 | 22 | /** 23 | * Generates models defined in a collection of files. 24 | * @param {string[]} filenames - The files to extract models from. 25 | * @returns {Map} 26 | */ 27 | function generateModelsFromFiles(filenames) { 28 | const program = ts.createProgram(filenames, { target: ts.ScriptTarget.ES5 }); 29 | const models = new Map(); 30 | 31 | for (const filename of filenames) { 32 | const sourceFile = program.getSourceFile(filename); 33 | 34 | if (sourceFile === undefined) { 35 | throw new Error(`cannot find source for file: '${filename}'`); 36 | } 37 | 38 | sourceFile.forEachChild((node) => { 39 | if (!ts.isTypeAliasDeclaration(node)) { 40 | return; 41 | } 42 | 43 | // @ts-ignore 44 | const config = getModelConfigFromComments(node.jsDoc); 45 | if (config === undefined) { 46 | return; 47 | } 48 | 49 | const model = new Model(program, node, config); 50 | models.set(model.kind, model); 51 | }); 52 | } 53 | 54 | return models; 55 | } 56 | 57 | /** 58 | * Extracts a model configuration from JSDoc comments. 59 | * @param {any[]} [jsDocs] 60 | * @returns {ModelCommentConfig} 61 | */ 62 | function getModelConfigFromComments(jsDocs) { 63 | if (jsDocs === undefined) { 64 | return; 65 | } 66 | 67 | for (let i = 0; i < jsDocs.length; ++i) { 68 | const jsDoc = jsDocs[i]; 69 | if (jsDoc.tags?.[0].tagName.escapedText !== kModelTag) { 70 | continue; 71 | } 72 | 73 | let isNamespaced = true; 74 | let apiVersion; 75 | let description; 76 | let kind; 77 | let plural; 78 | let singular; 79 | 80 | for (let j = 1; j < jsDoc.tags.length; ++j) { 81 | const tag = jsDoc.tags[j]; 82 | const tagName = tag.tagName.escapedText; 83 | 84 | switch (tagName) { 85 | case kApiVersionTag : 86 | apiVersion = tag.comment; 87 | break; 88 | case kDescriptionTag : 89 | description = tag.comment; 90 | break; 91 | case kKindTag : 92 | kind = tag.comment; 93 | break; 94 | case kScopeTag : 95 | if (tag.comment === 'cluster') { 96 | isNamespaced = false; 97 | } 98 | break; 99 | case kPluralTag : 100 | plural = tag.comment; 101 | break; 102 | case kSingularTag : 103 | singular = tag.comment; 104 | break; 105 | } 106 | } 107 | 108 | return { apiVersion, description, kind, isNamespaced, plural, singular }; 109 | } 110 | } 111 | 112 | module.exports = { generateModelsFromFiles }; 113 | -------------------------------------------------------------------------------- /crdgen/lib/model.d.ts: -------------------------------------------------------------------------------- 1 | export type ModelCRD = { 2 | apiVersion: string; 3 | kind: string; 4 | metadata: object; 5 | spec: object; 6 | }; 7 | /** 8 | * @typedef {object} ModelCRD 9 | * @property {string} apiVersion 10 | * @property {string} kind 11 | * @property {object} metadata 12 | * @property {object} spec 13 | */ 14 | /** 15 | * Class representing a single custom resource model. 16 | */ 17 | export class Model { 18 | /** 19 | * Construct a model. 20 | * @param {ts.Program} program - TypeScript API Program. 21 | * @param {ts.TypeAliasDeclaration} node - TypeScript API TypeAliasDeclaration node. 22 | * @param {ModelCommentConfig} config - Model configuration extracted from JSDoc comments. 23 | */ 24 | constructor(program: ts.Program, node: ts.TypeAliasDeclaration, config: ModelCommentConfig); 25 | group: string; 26 | version: string; 27 | kind: string; 28 | listKind: string; 29 | singular: string; 30 | plural: any; 31 | description: string; 32 | isNamespaced: boolean; 33 | /** 34 | * Converts the Model to a CRD object that can be stringified to YAML. 35 | * @returns {ModelCRD} An object representation of the Model as a Kubernetes CRD. 36 | */ 37 | toCRD(): ModelCRD; 38 | #private; 39 | } 40 | import ts = require("typescript"); 41 | import type { ModelCommentConfig } from './index'; 42 | -------------------------------------------------------------------------------- /crdgen/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kubenode/crdgen", 3 | "version": "0.1.1", 4 | "description": "Kubernetes Custom Resource Definition Generator", 5 | "license": "MIT", 6 | "author": "Colin J. Ihrig (http://www.cjihrig.com/)", 7 | "main": "lib/index.js", 8 | "scripts": { 9 | "lint": "belly-button -f", 10 | "pretest": "npm run lint && tsc --noEmit", 11 | "test": "node --test --experimental-test-coverage", 12 | "types": "rm -f lib/*.d.ts && tsc" 13 | }, 14 | "dependencies": { 15 | "pluralize": "^8.0.0", 16 | "typescript": "^5.6.2" 17 | }, 18 | "devDependencies": { 19 | "belly-button": "^8.0.0" 20 | }, 21 | "homepage": "https://github.com/cjihrig/kubenode#readme", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/cjihrig/kubenode.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/cjihrig/kubenode/issues" 28 | }, 29 | "keywords": [ 30 | "kubernetes", 31 | "k8s", 32 | "crd" 33 | ], 34 | "directories": { 35 | "lib": "lib", 36 | "test": "test" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /crdgen/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "target": "es2022" 10 | }, 11 | "include": ["lib/**/*.js"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /extension-server/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /extension-server/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | -------------------------------------------------------------------------------- /extension-server/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Colin Ihrig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /extension-server/README.md: -------------------------------------------------------------------------------- 1 | # `@kubenode/extension-server` 2 | 3 | Utilities for creating Kubernetes API extension servers. 4 | -------------------------------------------------------------------------------- /extension-server/lib/server.d.ts: -------------------------------------------------------------------------------- 1 | export class Server { 2 | constructor(options: any); 3 | groupList: any[]; 4 | handlers: Map; 5 | paths: string[]; 6 | port: number; 7 | router: Map; 8 | requestHandler: any; 9 | server: import("https").Server; 10 | register(api: any): void; 11 | start(): Promise; 12 | } 13 | declare namespace _default { 14 | export { Server }; 15 | } 16 | export default _default; 17 | export type PromiseWithResolvers = { 18 | promise: Promise; 19 | resolve: Function; 20 | reject: Function; 21 | }; 22 | -------------------------------------------------------------------------------- /extension-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kubenode/extension-server", 3 | "version": "0.1.0", 4 | "description": "Utilities for creating Kubernetes API extension servers", 5 | "license": "MIT", 6 | "author": "Colin J. Ihrig (http://www.cjihrig.com/)", 7 | "type": "module", 8 | "main": "lib/index.js", 9 | "scripts": { 10 | "//": "The linter is not happy about the move to ESM so it has been removed from the pretest script.", 11 | "lint": "belly-button -f", 12 | "pretest": "tsc --noEmit", 13 | "test": "node --test --experimental-test-coverage", 14 | "types": "rm -f lib/*.d.ts && tsc" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "^22.7.5", 18 | "belly-button": "^8.0.0", 19 | "typescript": "^5.6.2" 20 | }, 21 | "homepage": "https://github.com/cjihrig/kubenode#readme", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/cjihrig/kubenode.git" 25 | }, 26 | "bugs": { 27 | "url": "https://github.com/cjihrig/kubenode/issues" 28 | }, 29 | "keywords": [ 30 | "kubernetes", 31 | "extension", 32 | "server" 33 | ], 34 | "directories": { 35 | "lib": "lib", 36 | "test": "test" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /extension-server/test/server.js: -------------------------------------------------------------------------------- 1 | import { test } from 'node:test'; 2 | import { Server } from '../lib/server.js'; 3 | 4 | test('todo'); 5 | -------------------------------------------------------------------------------- /extension-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "target": "es2022" 10 | }, 11 | "include": ["lib/**/*.js"], 12 | "exclude": ["node_modules"] 13 | } 14 | -------------------------------------------------------------------------------- /kubenode/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /kubenode/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | -------------------------------------------------------------------------------- /kubenode/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Colin Ihrig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /kubenode/kubenode.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | const { run } = require('@kubenode/cli'); 4 | 5 | async function main() { 6 | try { 7 | await run(process.argv.slice(2)); 8 | } catch (err) { 9 | console.log(err.message); 10 | } 11 | } 12 | 13 | main(); 14 | -------------------------------------------------------------------------------- /kubenode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kubenode", 3 | "version": "0.3.0", 4 | "description": "Kubernetes and Node.js", 5 | "license": "MIT", 6 | "author": "Colin J. Ihrig (http://www.cjihrig.com/)", 7 | "bin": { 8 | "kubenode": "./kubenode.js" 9 | }, 10 | "scripts": {}, 11 | "dependencies": { 12 | "@kubenode/cli": "^0.1.10" 13 | }, 14 | "homepage": "https://github.com/cjihrig/kubenode#readme", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/cjihrig/kubenode.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/cjihrig/kubenode/issues" 21 | }, 22 | "keywords": [ 23 | "kubernetes", 24 | "k8s" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /reference/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /reference/.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !lib/** 3 | -------------------------------------------------------------------------------- /reference/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Colin Ihrig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /reference/README.md: -------------------------------------------------------------------------------- 1 | # `@kubenode/reference` 2 | 3 | Utilities for working with container image references. 4 | 5 | ## Functions 6 | 7 |
8 |
parse(input)Reference
9 |

parse() splits an image reference into its various components.

10 |
11 |
12 | 13 | ## Typedefs 14 | 15 |
16 |
NamedRepository : Object
17 |
18 |
Reference : Object
19 |
20 |
21 | 22 | 23 | 24 | ## parse(input) ⇒ [Reference](#Reference) 25 | parse() splits an image reference into its various components. 26 | 27 | **Kind**: global function 28 | 29 | | Param | Type | Description | 30 | | --- | --- | --- | 31 | | input | string | The image reference string to parse. | 32 | 33 | 34 | 35 | ## NamedRepository : Object 36 | **Kind**: global typedef 37 | **Properties** 38 | 39 | | Name | Type | Description | 40 | | --- | --- | --- | 41 | | [domain] | string | The repository host and port information. | 42 | | [path] | string | The image path. | 43 | 44 | 45 | 46 | ## Reference : Object 47 | **Kind**: global typedef 48 | **Properties** 49 | 50 | | Name | Type | Description | 51 | | --- | --- | --- | 52 | | namedRepository | [NamedRepository](#NamedRepository) | Object holding the image host, port, and path information. | 53 | | [tag] | string | The image tag if one exists. | 54 | | [digest] | string | The image digest if one exists. | 55 | 56 | ## Acknowledgment 57 | 58 | This package was adapted from the Golang 59 | [`distribution/reference`](https://pkg.go.dev/github.com/distribution/reference) 60 | package. 61 | -------------------------------------------------------------------------------- /reference/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace _exports { 2 | export { NamedRepository, Reference }; 3 | } 4 | declare namespace _exports { 5 | export { parse }; 6 | export namespace regex { 7 | export { anchoredDigestRegEx as anchoredDigest }; 8 | export { anchoredIdentifierRegEx as anchoredIdentifier }; 9 | export { anchoredNameRegEx as anchoredName }; 10 | export { anchoredTagRegEx as anchoredTag }; 11 | export { digestRegEx as digest }; 12 | export { domainRegEx as domain }; 13 | export { identifierRegEx as identifier }; 14 | export { nameRegEx as name }; 15 | export { referenceRegEx as reference }; 16 | export { tagRegEx as tag }; 17 | } 18 | } 19 | export = _exports; 20 | type NamedRepository = { 21 | /** 22 | * The repository host and port information. 23 | */ 24 | domain?: string; 25 | /** 26 | * The image path. 27 | */ 28 | path?: string; 29 | }; 30 | type Reference = { 31 | /** 32 | * Object holding the image host, port, and path information. 33 | */ 34 | namedRepository: NamedRepository; 35 | /** 36 | * The image tag if one exists. 37 | */ 38 | tag?: string; 39 | /** 40 | * The image digest if one exists. 41 | */ 42 | digest?: string; 43 | }; 44 | /** 45 | * @typedef {Object} NamedRepository 46 | * @property {string} [domain] The repository host and port information. 47 | * @property {string} [path] The image path. 48 | */ 49 | /** 50 | * @typedef {Object} Reference 51 | * @property {NamedRepository} namedRepository Object holding the image host, port, and path information. 52 | * @property {string} [tag] The image tag if one exists. 53 | * @property {string} [digest] The image digest if one exists. 54 | */ 55 | /** 56 | * parse() splits an image reference into its various components. 57 | * @param {string} input The image reference string to parse. 58 | * @returns {Reference} 59 | */ 60 | declare function parse(input: string): Reference; 61 | declare const anchoredDigestRegEx: RegExp; 62 | declare const anchoredIdentifierRegEx: RegExp; 63 | declare const anchoredNameRegEx: RegExp; 64 | declare const anchoredTagRegEx: RegExp; 65 | declare const digestRegEx: RegExp; 66 | declare const domainRegEx: RegExp; 67 | declare const identifierRegEx: RegExp; 68 | declare const nameRegEx: RegExp; 69 | declare const referenceRegEx: RegExp; 70 | declare const tagRegEx: RegExp; 71 | -------------------------------------------------------------------------------- /reference/lib/index.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | const kRepositoryNameTotalLengthMax = 255; 4 | 5 | const alphanumeric = '[a-z0-9]+'; 6 | const digestPat = '[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][0-9A-Fa-f]{32,}'; 7 | const domainNameComponent = '(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])'; 8 | const identifier = '([a-f0-9]{64})'; 9 | const ipv6address = '\\[(?:[a-fA-F0-9:]+)\\]'; 10 | const optionalPort = '(?::[0-9]+)?'; 11 | const separator = '(?:[._]|__|[-]+)'; 12 | const tag = '[\\w][\\w.-]{0,127}'; 13 | 14 | const domainName = domainNameComponent + anyTimes('\\.' + domainNameComponent); 15 | const host = '(?:' + domainName + '|' + ipv6address + ')'; 16 | const domainAndPort = host + optionalPort; 17 | const pathComponent = alphanumeric + anyTimes(separator, alphanumeric); 18 | const remoteName = pathComponent + anyTimes('/', pathComponent); 19 | const namePat = optional(domainAndPort, '/') + remoteName; 20 | const referencePat = anchored(capture(namePat), optional(':', capture(tag)), optional('@', capture(digestPat))); 21 | 22 | const anchoredDigestRegEx = new RegExp(anchored(digestPat)); 23 | const anchoredIdentifierRegEx = new RegExp(anchored(identifier)); 24 | const anchoredNameRegEx = new RegExp(anchored(optional(capture(domainAndPort), '/'), capture(remoteName))); 25 | const anchoredTagRegEx = new RegExp(anchored(tag)); 26 | const digestRegEx = new RegExp(digestPat); 27 | const domainRegEx = new RegExp(domainAndPort); 28 | const identifierRegEx = new RegExp(identifier); 29 | const nameRegEx = new RegExp(namePat); 30 | const referenceRegEx = new RegExp(referencePat); 31 | const tagRegEx = new RegExp(tag); 32 | 33 | function anchored(...strings) { 34 | return '^' + strings.join('') + '$'; 35 | } 36 | 37 | function anyTimes(...strings) { 38 | return '(?:' + strings.join('') + ')*'; 39 | } 40 | 41 | function capture(...strings) { 42 | return '(' + strings.join('') + ')'; 43 | } 44 | 45 | function optional(...strings) { 46 | return '(?:' + strings.join('') + ')?'; 47 | } 48 | 49 | /** 50 | * @typedef {Object} NamedRepository 51 | * @property {string} [domain] The repository host and port information. 52 | * @property {string} [path] The image path. 53 | */ 54 | 55 | /** 56 | * @typedef {Object} Reference 57 | * @property {NamedRepository} namedRepository Object holding the image host, port, and path information. 58 | * @property {string} [tag] The image tag if one exists. 59 | * @property {string} [digest] The image digest if one exists. 60 | */ 61 | 62 | /** 63 | * parse() splits an image reference into its various components. 64 | * @param {string} input The image reference string to parse. 65 | * @returns {Reference} 66 | */ 67 | function parse(input) { 68 | if (typeof input !== 'string') { 69 | throw new TypeError('input must be a string'); 70 | } 71 | 72 | const match = referenceRegEx.exec(input); 73 | 74 | if (match === null) { 75 | if (input === '') { 76 | throw new Error('repository name must have at least one component'); 77 | } 78 | 79 | if (referenceRegEx.test(input.toLowerCase())) { 80 | throw new Error('repository name must be lowercase'); 81 | } 82 | 83 | throw new Error('invalid reference format'); 84 | } 85 | 86 | const nameMatch = anchoredNameRegEx.exec(match[1]); 87 | const reference = { 88 | namedRepository: { 89 | domain: undefined, 90 | path: undefined 91 | }, 92 | tag: match[2], 93 | digest: undefined 94 | }; 95 | 96 | if (nameMatch.length === 3) { 97 | reference.namedRepository.domain = nameMatch[1]; 98 | reference.namedRepository.path = nameMatch[2]; 99 | } else { 100 | reference.namedRepository.domain = ''; 101 | reference.namedRepository.path = match[1]; 102 | } 103 | 104 | if (reference.namedRepository.path.length > kRepositoryNameTotalLengthMax) { 105 | throw new RangeError('repository name must not be more than ' + 106 | `${kRepositoryNameTotalLengthMax} characters`); 107 | } 108 | 109 | if (match[3] !== undefined) { 110 | reference.digest = match[3]; 111 | } 112 | 113 | return reference; 114 | } 115 | 116 | module.exports = { 117 | parse, 118 | regex: { 119 | anchoredDigest: anchoredDigestRegEx, 120 | anchoredIdentifier: anchoredIdentifierRegEx, 121 | anchoredName: anchoredNameRegEx, 122 | anchoredTag: anchoredTagRegEx, 123 | digest: digestRegEx, 124 | domain: domainRegEx, 125 | identifier: identifierRegEx, 126 | name: nameRegEx, 127 | reference: referenceRegEx, 128 | tag: tagRegEx 129 | } 130 | }; 131 | -------------------------------------------------------------------------------- /reference/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kubenode/reference", 3 | "version": "0.1.1", 4 | "description": "Utilities for working with container image references", 5 | "license": "MIT", 6 | "author": "Colin J. Ihrig (http://www.cjihrig.com/)", 7 | "main": "lib/index.js", 8 | "scripts": { 9 | "lint": "belly-button -f", 10 | "pretest": "npm run lint && tsc --noEmit", 11 | "test": "node --test --experimental-test-coverage", 12 | "types": "rm -f lib/*.d.ts && tsc" 13 | }, 14 | "devDependencies": { 15 | "belly-button": "^8.0.0", 16 | "typescript": "^5.6.2" 17 | }, 18 | "homepage": "https://github.com/cjihrig/kubenode#readme", 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/cjihrig/kubenode.git" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/cjihrig/kubenode/issues" 25 | }, 26 | "keywords": [ 27 | "container", 28 | "image", 29 | "reference" 30 | ], 31 | "directories": { 32 | "lib": "lib", 33 | "test": "test" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /reference/test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const assert = require('node:assert'); 3 | const { test } = require('node:test'); 4 | const { parse } = require('../lib'); 5 | 6 | test('throws if input is not a string', () => { 7 | assert.throws(() => { 8 | parse(); 9 | }, /TypeError: input must be a string/); 10 | }); 11 | 12 | test('throws if repository has no components', () => { 13 | assert.throws(() => { 14 | parse(''); 15 | }, /repository name must have at least one component/); 16 | }); 17 | 18 | test('throws if repository name is not all lowercase', () => { 19 | assert.throws(() => { 20 | parse('LOCALHOST'); 21 | }, /repository name must be lowercase/); 22 | }); 23 | 24 | test('throws if repository name is invalid', () => { 25 | assert.throws(() => { 26 | parse(':::::'); 27 | }, /invalid reference format/); 28 | }); 29 | 30 | test('throws if repository name is too long', () => { 31 | assert.throws(() => { 32 | parse('x'.repeat(256)); 33 | }, /RangeError: repository name must not be more than 255 characters/); 34 | }); 35 | 36 | test('repository path only', () => { 37 | const r = parse('node'); 38 | assert.deepStrictEqual(r, { 39 | namedRepository: { domain: undefined, path: 'node' }, 40 | tag: undefined, 41 | digest: undefined 42 | }); 43 | }); 44 | 45 | test('repository domain and path', () => { 46 | const r = parse('localhost/node'); 47 | assert.deepStrictEqual(r, { 48 | namedRepository: { domain: 'localhost', path: 'node' }, 49 | tag: undefined, 50 | digest: undefined 51 | }); 52 | }); 53 | 54 | test('repository domain and multipart path', () => { 55 | const r = parse('localhost/node/node'); 56 | assert.deepStrictEqual(r, { 57 | namedRepository: { domain: 'localhost', path: 'node/node' }, 58 | tag: undefined, 59 | digest: undefined 60 | }); 61 | }); 62 | 63 | test('repository domain, port, and path', () => { 64 | const r = parse('localhost:5000/node'); 65 | assert.deepStrictEqual(r, { 66 | namedRepository: { domain: 'localhost:5000', path: 'node' }, 67 | tag: undefined, 68 | digest: undefined 69 | }); 70 | }); 71 | 72 | test('repository path and tag', () => { 73 | const r = parse('node:latest'); 74 | assert.deepStrictEqual(r, { 75 | namedRepository: { domain: undefined, path: 'node' }, 76 | tag: 'latest', 77 | digest: undefined 78 | }); 79 | }); 80 | 81 | test('reference with all components', () => { 82 | const r = parse('localhost:5000/controller:latest@sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf'); 83 | assert.deepStrictEqual(r, { 84 | namedRepository: { domain: 'localhost:5000', path: 'controller' }, 85 | tag: 'latest', 86 | digest: 'sha256:cbbf2f9a99b47fc460d422812b6a5adff7dfee951d8fa2e4a98caa0382cfbdbf' 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /reference/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "moduleResolution": "node", 8 | "skipLibCheck": true, 9 | "target": "es2022" 10 | }, 11 | "include": ["lib/**/*.js"], 12 | "exclude": ["node_modules"] 13 | } 14 | --------------------------------------------------------------------------------