├── .gitignore ├── .prettierrc.yaml ├── bin └── generate-kubernetes-types ├── assets ├── package.json └── README.md ├── tsconfig.json ├── package.json ├── src ├── generate │ ├── util.ts │ └── index.ts └── openapi │ ├── index.ts │ └── generate.ts ├── README.md └── .github └── workflows └── update.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /types 3 | node_modules/ 4 | 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | bracketSpacing: false 2 | printWidth: 100 3 | semi: false 4 | singleQuote: true 5 | trailingComma: es5 6 | -------------------------------------------------------------------------------- /bin/generate-kubernetes-types: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as path from 'path'; 4 | import { fileURLToPath } from 'url'; 5 | 6 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 7 | 8 | await import(path.join(__dirname, '..', 'lib', 'generate', 'index.js')); -------------------------------------------------------------------------------- /assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kubernetes-types", 3 | "version": "", 4 | "description": "TypeScript definitions of Kubernetes resource types", 5 | "repository": "https://github.com/silverlyra/kubernetes-types", 6 | "author": "Lyra Naeseth ", 7 | "license": "Apache-2.0", 8 | "devDependencies": { 9 | "typescript": ">=3.2.0" 10 | } 11 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "lib": ["esnext"], 5 | "outDir": "lib", 6 | "rootDir": "src", 7 | "module": "nodenext", 8 | "resolveJsonModule": true, 9 | "declaration": true, 10 | "sourceMap": true, 11 | "esModuleInterop": true, 12 | 13 | "strict": true, 14 | "alwaysStrict": true, 15 | "noImplicitAny": true, 16 | "noImplicitThis": true, 17 | "strictFunctionTypes": true, 18 | "strictNullChecks": true, 19 | "strictPropertyInitialization": true, 20 | 21 | "noImplicitReturns": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | 25 | "pretty": true 26 | }, 27 | "include": ["src/**/*.ts"], 28 | "exclude": ["node_modules/**", "bin/**"] 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kubernetes-type-generator", 3 | "version": "0.1.0", 4 | "description": "Generate TypeScript definitions of Kubernetes resource types", 5 | "type": "module", 6 | "module": "lib/generate/index.mjs", 7 | "repository": "https://github.com/silverlyra/kubernetes-types", 8 | "author": "Lyra Naeseth ", 9 | "license": "Apache-2.0", 10 | "scripts": { 11 | "build": "tsc -p tsconfig.json" 12 | }, 13 | "dependencies": { 14 | "argparse": "^1.0.10", 15 | "mkdirp": "^3.0.1", 16 | "node-fetch": "^3.3.2", 17 | "ts-morph": "^22.0.0" 18 | }, 19 | "devDependencies": { 20 | "@types/argparse": "^1.0.35", 21 | "@types/node": "^20.12.12", 22 | "prettier": "^1.15.3", 23 | "typescript": "^5.0.0" 24 | } 25 | } -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # kubernetes-types 2 | 3 | This package provides TypeScript definitions for Kubernetes API types, generated from the Kubernetes OpenAPI definitions. 4 | 5 | ## Example 6 | 7 | ```typescript 8 | import {Pod} from 'kubernetes-types/core/v1' 9 | import {ObjectMeta} from 'kubernetes-types/meta/v1' 10 | 11 | let metadata: ObjectMeta = {name: 'example', labels: {app: 'example'}} 12 | let pod: Pod = { 13 | apiVersion: 'v1', 14 | kind: 'Pod', // 'v1' and 'Pod' are the only accepted values for a Pod 15 | 16 | metadata, 17 | 18 | spec: { 19 | containers: [ 20 | /* ... */ 21 | ], 22 | }, 23 | } 24 | ``` 25 | 26 | ## Versioning 27 | 28 | As an NPM package, kubernetes-types follows semver. The major and minor version of the package will track the Kubernetes API version, while the patch version will follow updates to the generated types. 29 | 30 | You should install the version of the types matching the Kubernetes API version you want to be compatible with. Consult [NPM][versions] for the list of available versions of this package. 31 | 32 | [versions]: https://www.npmjs.com/package/kubernetes-types?activeTab=versions 33 | -------------------------------------------------------------------------------- /src/generate/util.ts: -------------------------------------------------------------------------------- 1 | import {Project, SourceFile} from 'ts-morph' 2 | 3 | export class Imports { 4 | private imports: Map> = new Map() 5 | 6 | constructor(private file: SourceFile) {} 7 | 8 | public add(from: SourceFile, name: string): this { 9 | if (from === this.file) { 10 | return this 11 | } 12 | 13 | let fileImports = this.imports.get(from) 14 | if (fileImports == null) { 15 | fileImports = new Set() 16 | this.imports.set(from, fileImports) 17 | } 18 | 19 | fileImports.add(name) 20 | return this 21 | } 22 | 23 | public apply() { 24 | for (let [from, names] of this.imports) { 25 | let relativePath = this.file.getDirectory().getRelativePathAsModuleSpecifierTo(from) 26 | this.file.addImportDeclaration({ 27 | moduleSpecifier: relativePath, 28 | namedImports: [...names].sort(), 29 | }) 30 | } 31 | } 32 | } 33 | 34 | export const ensureFile = (proj: Project, path: string): SourceFile => { 35 | let sourceFile = proj.getSourceFile(path) 36 | if (sourceFile == null) { 37 | sourceFile = proj.createSourceFile(path) 38 | } 39 | return sourceFile 40 | } 41 | 42 | export const filePath = (importPath: string): string => `${importPath}.ts` 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubernetes-types 2 | 3 | This package provides TypeScript definitions for Kubernetes API types, generated from the Kubernetes OpenAPI definitions. 4 | 5 | ## Example 6 | 7 | ```typescript 8 | import {Pod} from 'kubernetes-types/core/v1' 9 | import {ObjectMeta} from 'kubernetes-types/meta/v1' 10 | 11 | let metadata: ObjectMeta = {name: 'example', labels: {app: 'example'}} 12 | let pod: Pod = { 13 | apiVersion: 'v1', 14 | kind: 'Pod', // 'v1' and 'Pod' are the only accepted values for a Pod 15 | 16 | metadata, 17 | 18 | spec: { 19 | containers: [ 20 | /* ... */ 21 | ], 22 | }, 23 | } 24 | ``` 25 | 26 | ## Versioning 27 | 28 | As an NPM package, kubernetes-types follows semver. The major and minor version of the package will track the Kubernetes API version, while the patch version will follow updates to the generated types. 29 | 30 | You should install the version of the types matching the Kubernetes API version you want to be compatible with. Consult [NPM][versions] for the list of available versions of this package. 31 | 32 | [versions]: https://www.npmjs.com/package/kubernetes-types?activeTab=versions 33 | 34 | ## This repository 35 | 36 | This repository contains the code used to generate the TypeScript types, not the types themselves. 37 | -------------------------------------------------------------------------------- /src/openapi/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An OpenAPI API. Only enough of it is implemented as we need. 3 | */ 4 | export interface API { 5 | info: APIInfo 6 | definitions: {[name: string]: Definition} 7 | } 8 | 9 | export interface APIInfo { 10 | title: string 11 | version: string 12 | } 13 | 14 | export interface Definition { 15 | description: string 16 | required?: string[] 17 | properties?: {[name: string]: Property} 18 | 'x-kubernetes-group-version-kind'?: GroupVersionKind[] 19 | } 20 | 21 | export interface GroupVersionKind { 22 | group: string 23 | version: string 24 | kind: string 25 | } 26 | 27 | export interface PropertyMeta { 28 | description: string 29 | } 30 | 31 | export type Property = PropertyMeta & Value 32 | 33 | export type Value = ScalarValue | ArrayValue | ObjectValue | Reference 34 | 35 | export interface Reference { 36 | $ref: string 37 | } 38 | 39 | export interface ScalarValue { 40 | type: 'string' | 'integer' | 'number' | 'boolean' 41 | } 42 | 43 | export interface ArrayValue { 44 | type: 'array' 45 | items: Value 46 | } 47 | 48 | export interface ObjectValue { 49 | type: 'object' 50 | additionalProperties: Value 51 | } 52 | 53 | export const resolve = (api: API, {$ref: ref}: Reference): {name: string; def: Definition} => { 54 | const prefix = '#/definitions/' 55 | if (!ref.startsWith(prefix)) { 56 | throw new Error(`Invalid or unsupported $ref: ${JSON.stringify(ref)}`) 57 | } 58 | 59 | let name = ref.slice(prefix.length) 60 | let def = api.definitions[name] 61 | if (def == null) { 62 | throw new Error(`Failed to resolve ${name} in ${api.info.title}/${api.info.version}.`) 63 | } 64 | 65 | return {name, def} 66 | } 67 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | name: "Check for updates" 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: >- 7 | 15 15 * * * 8 | 9 | jobs: 10 | check: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | packages: read 15 | steps: 16 | - name: Check for updates 17 | id: check 18 | uses: silverlyra/script-action@v0.2 19 | with: 20 | script: | 21 | const npmResponse = await fetch('https://registry.npmjs.org/kubernetes-types/'); 22 | const npmTags = (await npmResponse.json())['dist-tags']; 23 | const [major, minor] = npmTags.latest.split('.', 2).map(Number); 24 | 25 | const next = `${major}.${minor + 1}`; 26 | console.log(`Latest package: ${npmTags.latest}; checking for Kubernetes ${next}`); 27 | 28 | const { data: refs } = await github.rest.git.listMatchingRefs({ 29 | owner: 'kubernetes', 30 | repo: 'kubernetes', 31 | ref: `tags/v${next}.0`, 32 | }); 33 | 34 | // exclude pre-releases 35 | const ref = refs.find(({ ref }) => ref === `refs/tags/v${next}.0`); 36 | const found = ref != null; 37 | 38 | if (found) { 39 | console.log(`Found new release: ${next}.0`); 40 | 41 | const { data: {download_url: url} } = await github.rest.repos.getContent({ 42 | owner: 'kubernetes', 43 | repo: 'kubernetes', 44 | ref: `v${next}.0`, 45 | path: 'api/openapi-spec/swagger.json', 46 | }); 47 | 48 | const response = await fetch(url); 49 | const spec = await response.text(); 50 | 51 | await fs.writeFile(path.join(env.RUNNER_TEMP, 'swagger.json'), spec, 'utf-8'); 52 | console.log(`Saved swagger.json to ${env.RUNNER_TEMP}`); 53 | } 54 | 55 | return { next, found, ref }; 56 | - name: Checkout repository 57 | if: fromJson(steps.check.outputs.result).found 58 | uses: actions/checkout@v4 59 | - name: Setup node.js 60 | if: fromJson(steps.check.outputs.result).found 61 | uses: actions/setup-node@v4 62 | with: 63 | node-version: '20.x' 64 | registry-url: 'https://registry.npmjs.org' 65 | cache: npm 66 | - name: Update types 67 | if: fromJson(steps.check.outputs.result).found 68 | id: update 69 | env: 70 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 71 | VERSION: v${{ fromJson(steps.check.outputs.result).next }} 72 | run: | 73 | npm ci 74 | npm run build 75 | 76 | echo "Generating type package for ${VERSION}" 77 | mkdir -p types 78 | 79 | ./bin/generate-kubernetes-types --api "$VERSION" --file "${RUNNER_TEMP}/swagger.json" 80 | 81 | cd "./types/${VERSION}.0" 82 | npm publish -------------------------------------------------------------------------------- /src/generate/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * kubernetes-types-generator 3 | * 4 | * Generates TypeScript types for Kubernetes API resources. 5 | */ 6 | 7 | import {ArgumentParser} from 'argparse' 8 | import {readFileSync, writeFileSync} from 'fs' 9 | import {sync as mkdirpSync} from 'mkdirp' 10 | import fetch from 'node-fetch' 11 | import * as path from 'path' 12 | import {Project, ScriptTarget} from 'ts-morph' 13 | import {fileURLToPath} from 'url' 14 | 15 | import {API} from '../openapi/index.js' 16 | import generate from '../openapi/generate.js' 17 | 18 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 19 | const assetsPath = path.normalize(path.join(__dirname, '..', '..', 'assets')) 20 | 21 | interface Arguments { 22 | api: string 23 | file: string | undefined 24 | patch: number 25 | beta: number | undefined 26 | } 27 | 28 | async function main({api: apiVersion, file, patch, beta}: Arguments) { 29 | apiVersion = normalizeVersion(apiVersion) 30 | 31 | let api: API = file ? JSON.parse(readFileSync(file, 'utf8')) : await fetchAPI(apiVersion) 32 | 33 | let proj = new Project({ 34 | compilerOptions: {target: ScriptTarget.ES2016, declaration: true}, 35 | useInMemoryFileSystem: true, 36 | }) 37 | 38 | generate(proj, api) 39 | let result = proj.emitToMemory({emitOnlyDtsFiles: true}) 40 | let files = result.getFiles() 41 | 42 | const version = releaseVersion(apiVersion, {patch, beta}) 43 | const destPath = path.normalize(path.join(__dirname, '..', '..', 'types', `v${version}`)) 44 | for (let {filePath, text} of files) { 45 | let destFilePath = path.join(destPath, filePath.replace(/^\//, '')) 46 | mkdirpSync(path.dirname(destFilePath)) 47 | writeFileSync(destFilePath, text, 'utf8') 48 | console.log(`v${version}${filePath}`) 49 | } 50 | 51 | let generatedPackage = JSON.parse(readFileSync(path.join(assetsPath, 'package.json'), 'utf8')) 52 | generatedPackage.version = version 53 | writeFileSync( 54 | path.join(destPath, 'package.json'), 55 | JSON.stringify(generatedPackage, null, 2), 56 | 'utf8' 57 | ) 58 | 59 | writeFileSync( 60 | path.join(destPath, 'README.md'), 61 | readFileSync(path.join(assetsPath, 'README.md'), 'utf8'), 62 | 'utf8' 63 | ) 64 | } 65 | 66 | function normalizeVersion(version: string): string { 67 | if (/^\d/.test(version)) { 68 | version = `v${version}` 69 | } 70 | if (/^v\d+\.\d+$/.test(version)) { 71 | version = `${version}.0` 72 | } 73 | 74 | return version 75 | } 76 | 77 | async function fetchAPI(version: string): Promise { 78 | let response = await fetch( 79 | `https://raw.githubusercontent.com/kubernetes/kubernetes/${version}/api/openapi-spec/swagger.json` 80 | ) 81 | 82 | let api = await response.json() 83 | return api as API 84 | } 85 | 86 | function releaseVersion( 87 | apiVersion: string, 88 | {patch, beta}: Pick 89 | ): string { 90 | let [major, minor] = apiVersion.replace(/^v/, '').split('.') 91 | let version = `${major}.${minor}.${patch}` 92 | if (beta) { 93 | version += `-beta.${beta}` 94 | } 95 | return version 96 | } 97 | 98 | const parser = new ArgumentParser({ 99 | description: 'Generate TypeScript types for the Kubernetes API', 100 | }) 101 | parser.addArgument(['-a', '--api'], {help: 'Kubernetes API version', defaultValue: 'master'}) 102 | parser.addArgument(['-f', '--file'], {help: 'Path to local swagger.json file'}) 103 | parser.addArgument(['-p', '--patch'], { 104 | help: 'Patch version of generated types', 105 | type: Number, 106 | defaultValue: 0, 107 | }) 108 | parser.addArgument('--beta', {help: 'Create a beta release', type: Number}) 109 | 110 | main(parser.parseArgs()).catch(err => { 111 | console.error(err.stack) 112 | process.exit(1) 113 | }) 114 | -------------------------------------------------------------------------------- /src/openapi/generate.ts: -------------------------------------------------------------------------------- 1 | import {Project, PropertySignatureStructure, StructureKind} from 'ts-morph' 2 | 3 | import {ensureFile, filePath, Imports} from '../generate/util.js' 4 | import {API, Definition, GroupVersionKind, resolve, Value} from './index.js' 5 | 6 | export default function generate(proj: Project, api: API) { 7 | let imports: Map = new Map() 8 | 9 | for (let {name, path, def} of definitions(api)) { 10 | if (name in elidedTypes) { 11 | continue 12 | } 13 | 14 | let file = ensureFile(proj, filePath(path)) 15 | let fileImports = imports.get(file.getFilePath()) 16 | if (fileImports == null) { 17 | fileImports = new Imports(file) 18 | imports.set(file.getFilePath(), fileImports) 19 | } 20 | 21 | if (name in scalarTypes) { 22 | file.addTypeAlias({ 23 | name, 24 | isExported: true, 25 | type: scalarTypes[name], 26 | docs: def.description ? [{description: def.description}] : [], 27 | }) 28 | } else { 29 | file.addInterface({ 30 | name, 31 | isExported: true, 32 | properties: properties(proj, api, def, fileImports), 33 | docs: def.description ? [{description: def.description}] : [], 34 | }) 35 | } 36 | } 37 | 38 | for (let fileImports of imports.values()) { 39 | fileImports.apply() 40 | } 41 | } 42 | 43 | interface ResolvedDefinition { 44 | name: string 45 | path: string 46 | def: Definition 47 | } 48 | 49 | export function definitions(api: API): ResolvedDefinition[] { 50 | let defs = [] 51 | 52 | for (let name of Object.keys(api.definitions)) { 53 | let parsed = parseDefName(name) 54 | if (parsed == null) { 55 | continue 56 | } 57 | 58 | defs.push({...parsed, def: api.definitions[name]}) 59 | } 60 | 61 | return defs 62 | } 63 | 64 | export function properties( 65 | proj: Project, 66 | api: API, 67 | {required, properties: props, 'x-kubernetes-group-version-kind': gvk}: Definition, 68 | imports: Imports 69 | ): PropertySignatureStructure[] { 70 | if (!props) { 71 | return [] 72 | } 73 | 74 | return Object.keys(props).map(name => { 75 | let prop = props[name] 76 | return { 77 | name, 78 | kind: StructureKind.PropertySignature, 79 | type: kindType(gvk, name) || type(proj, api, imports, prop), 80 | docs: prop.description ? [prop.description] : [], 81 | hasQuestionToken: !(required || []).includes(name), 82 | isReadonly: prop.description ? prop.description.includes('Read-only.') : false, 83 | } 84 | }) 85 | } 86 | 87 | export function kindType( 88 | gvkList: GroupVersionKind[] | undefined, 89 | propName: string 90 | ): string | undefined { 91 | if (gvkList != null && gvkList.length === 1) { 92 | const gvk = gvkList[0] 93 | if (propName === 'apiVersion') { 94 | return JSON.stringify([gvk.group, gvk.version].filter(Boolean).join('/')) 95 | } else if (propName === 'kind') { 96 | return JSON.stringify(gvk.kind) 97 | } 98 | } 99 | 100 | return undefined 101 | } 102 | 103 | export function type(proj: Project, api: API, imports: Imports, value: Value): string { 104 | let t = '' 105 | 106 | if ('$ref' in value) { 107 | let ref = parseDefName(resolve(api, value).name) 108 | if (ref == null) { 109 | throw new Error(`Value references excluded type: ${JSON.stringify(value)}`) 110 | } 111 | 112 | if (ref.name in elidedTypes) { 113 | t = elidedTypes[ref.name] 114 | } else { 115 | imports.add(ensureFile(proj, filePath(ref.path)), ref.name) 116 | t = ref.name 117 | } 118 | } else if ('type' in value) { 119 | switch (value.type) { 120 | case 'string': 121 | case 'number': 122 | case 'boolean': 123 | t = value.type 124 | break 125 | case 'integer': 126 | t = 'number' 127 | break 128 | case 'object': 129 | t = `{[name: string]: ${type(proj, api, imports, value.additionalProperties)}}` 130 | break 131 | case 'array': 132 | t = `Array<${type(proj, api, imports, value.items)}>` 133 | break 134 | default: 135 | assertNever(value) 136 | } 137 | } else { 138 | assertNever(value) 139 | } 140 | 141 | return t 142 | } 143 | 144 | const simplifyDefName = (name: string): string | undefined => { 145 | const simplifications = { 146 | 'io.k8s.api.': '', 147 | 'io.k8s.apimachinery.pkg.apis.': '', 148 | 'io.k8s.apimachinery.pkg.': '', 149 | 'io.k8s.apiextensions-apiserver.pkg.apis.': '', 150 | } 151 | for (let [prefix, replacement] of Object.entries(simplifications)) { 152 | if (name.startsWith(prefix)) { 153 | return `${replacement}${name.slice(prefix.length)}` 154 | } 155 | } 156 | 157 | return undefined 158 | } 159 | 160 | export function parseDefName(name: string): {name: string; path: string} | undefined { 161 | let simplifiedName = simplifyDefName(name) 162 | if (simplifiedName == null) { 163 | return undefined 164 | } 165 | name = simplifiedName 166 | 167 | let parts = name.split('.') 168 | name = parts[parts.length - 1] 169 | let path = parts.slice(0, -1).join('/') 170 | 171 | return {name, path} 172 | } 173 | 174 | const elidedTypes: {[name: string]: string} = { 175 | IntOrString: 'number | string', 176 | } 177 | const scalarTypes: {[name: string]: string} = { 178 | Quantity: 'string', 179 | Time: 'string', 180 | MicroTime: 'string', 181 | JSONSchemaPropsOrArray: 'JSONSchemaProps | JSONSchemaProps[]', 182 | JSONSchemaPropsOrBool: 'JSONSchemaProps | boolean', 183 | JSONSchemaPropsOrStringArray: 'JSONSchemaProps | string[]', 184 | } 185 | 186 | const assertNever = (_: never) => { 187 | throw new Error('"unreachable" code was reached') 188 | } 189 | --------------------------------------------------------------------------------