-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: initialize @kubekit/prepare project for generating Kubernetes O…
…penAPI specs based on ServiceAccount permissions
- Loading branch information
1 parent
617edc3
commit 50d3960
Showing
24 changed files
with
47,447 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
tmp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# Introduction | ||
|
||
The `@kubekit/prepare` project is a tool that generates a JSON file of OpenAPI specifications that describe operations that can be executed based on the permissions of Kubernetes ServiceAccount. This tool is particularly useful for application developers who use Kubernetes API. The generated `openapi.json` file is used as a schema file to generate client code in the `@kubekit/codegen` project. | ||
|
||
## Key Features | ||
|
||
- **Accurate Permission Checks**: Generates OpenAPI specifications that only include API operations that can be executed based on the permissions of the ServiceAccount. | ||
- **Scalability**: Currently, only `ServiceAccount` is supported, but support for `User` and `Group` is also planned in the future. | ||
- **Strict Type Definitions**: Provides more strict type definitions than the standard Swagger definitions of Kubernetes, promoting safer API usage. | ||
|
||
## Configuration | ||
|
||
To use the project, you need to configure the `config.ts` file. The following example shows how to configure a [ServiceAccount](file:///Users/kahiro/Documents/appthrust/kubekit-ts/packages/kubekit-prepare/README.md#3%2C22-3%2C22). | ||
|
||
```typescript:config.ts | ||
import { ConfigFile } from "./config" | ||
|
||
const config: ConfigFile = { | ||
kind: 'ServiceAccount', | ||
name: 'replicaset-controller', | ||
namespace: 'kube-system', | ||
outputFile: './replicaset-controller.openapi.json', | ||
} | ||
|
||
export default config | ||
``` | ||
|
||
## Execution Method | ||
|
||
After creating the configuration file, execute the following command to generate `replicaset-controller.openapi.json`. | ||
|
||
```bash | ||
npx @kubekit/prepare config.ts | ||
``` | ||
|
||
## Future Prospects | ||
|
||
This project is currently under development and plans to add support for `User` and `Group`, as well as further feature enhancements. This is aimed at enabling more users to effectively utilize Kubernetes API. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import type { ConfigFile } from '@kubekit/codegen' | ||
|
||
const config: ConfigFile = { | ||
schemaFile: '../openapi/apis/rbac.authorization.k8s.io/v1/swagger.json', | ||
apiFile: '@kubekit/client', | ||
outputFile: '../src/k8s-client/rbac-v1.ts', | ||
} | ||
|
||
export default config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import type { ConfigFile } from '@kubekit/codegen' | ||
|
||
const config: ConfigFile = { | ||
schemaFile: '../openapi/api/v1/swagger.json', | ||
apiFile: '@kubekit/client', | ||
outputFile: '../src/k8s-client/v1.ts', | ||
} | ||
|
||
export default config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
{ | ||
"name": "@kubekit/prepare", | ||
"version": "0.0.5", | ||
"main": "lib/index.js", | ||
"types": "lib/index.d.ts", | ||
"author": "kahirokunn", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/appthrust/kubekit-ts.git" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/appthrust/kubekit-ts/issues" | ||
}, | ||
"homepage": "https://github.com/appthrust/kubekit-ts", | ||
"bin": "lib/bin/cli.js", | ||
"scripts": { | ||
"build": "tsc", | ||
"prepare": "tsc && chmod +x ./lib/bin/cli.js", | ||
"sync": "npx @kubekit/sync ./", | ||
"gen": "npx @kubekit/codegen gen-config/v1.ts && npx @kubekit/codegen gen-config/rbac-v1.ts", | ||
"cli": "lib/bin/cli.js", | ||
"test1": "yarn build; chmod +x lib/bin/cli.js; yarn cli tests/test1.config.ts", | ||
"test2": "yarn build; chmod +x lib/bin/cli.js; yarn cli tests/test2.config.ts" | ||
}, | ||
"files": [ | ||
"lib", | ||
"src" | ||
], | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "pretty-quick --staged" | ||
} | ||
}, | ||
"devDependencies": { | ||
"@kubekit/codegen": "^0.0.19", | ||
"@kubekit/sync": "^0.0.21", | ||
"@types/lodash.merge": "^4.6.9", | ||
"@types/node": "^20.11.30", | ||
"esbuild": "^0.21.1", | ||
"esbuild-runner": "^2.2.2", | ||
"openapi-types": "^12.1.3", | ||
"typescript": "5.4.5" | ||
}, | ||
"dependencies": { | ||
"@kubekit/client": "0.0.23", | ||
"commander": "^6.2.0", | ||
"lodash.merge": "^4.6.2" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
#!/usr/bin/env node | ||
|
||
import program from 'commander' | ||
import * as path from 'path' | ||
import * as fs from 'fs/promises' | ||
import { patchFunctions } from '../patchOpenApi' | ||
import merge from 'lodash.merge' | ||
import { OpenAPIV3 } from 'openapi-types' | ||
import { type ResourceRules, inspectRules } from '../getRules' | ||
import { assertNotNull, mapK8sVerbToHttpMethod } from '../lib' | ||
import { apiClient } from '@kubekit/client' | ||
import { ConfigFile } from '../config' | ||
|
||
let ts = false | ||
try { | ||
if (require.resolve('esbuild') && require.resolve('esbuild-runner')) { | ||
require('esbuild-runner/register') | ||
} | ||
ts = true | ||
} catch {} | ||
|
||
// tslint:disable-next-line | ||
const meta = require('../../package.json') | ||
|
||
program.version(meta.version).usage('</path/to/config.js>').parse(process.argv) | ||
|
||
const configFilePath = program.args[0] | ||
|
||
if ( | ||
program.args.length === 0 || | ||
!/\.(c?(jsx?|tsx?)|jsonc?)?$/.test(configFilePath) | ||
) { | ||
program.help() | ||
} else { | ||
if (/\.tsx?$/.test(configFilePath) && !ts) { | ||
console.error( | ||
'Encountered a TypeScript configfile, but neither esbuild-runner nor ts-node are installed.' | ||
) | ||
process.exit(1) | ||
} | ||
run(path.resolve(process.cwd(), configFilePath)) | ||
} | ||
async function run(configFilePath: string) { | ||
process.chdir(path.dirname(configFilePath)) | ||
|
||
const unparsedConfig = require(configFilePath) | ||
const config: ConfigFile = unparsedConfig.default ?? unparsedConfig | ||
if (config.kind === 'Group' || config.kind === 'User') { | ||
console.error('Group and User are not yet supported. Please open an issue.') | ||
process.exit(1) | ||
} | ||
try { | ||
console.log(`Generating ${config.outputFile}`) | ||
await generateOpenApi(config) | ||
console.log(`Done`) | ||
} catch (err) { | ||
console.error(err) | ||
process.exit(1) | ||
} | ||
} | ||
|
||
async function generateOpenApi(config: ConfigFile) { | ||
const [ | ||
{ | ||
resourceRules, | ||
// TODO: Implement nonResourceRules as well | ||
nonResourceRules: _, | ||
}, | ||
rootOpenApiText, | ||
] = await Promise.all([ | ||
inspectRules(config.kind, config.name, config.namespace), | ||
apiClient<string>({ path: '/openapi/v3' }), | ||
]) | ||
|
||
const source: OpenAPIV3.Document<{}> = JSON.parse(rootOpenApiText) | ||
|
||
const cwd = process.cwd() | ||
|
||
const sourcePaths = Object.keys(source.paths) | ||
const docs: OpenAPIV3.Document<{}>[] = [] | ||
|
||
const addDoc = async (path: string, rule: ResourceRules[string]) => { | ||
const sourceDoc = await apiClient<OpenAPIV3.Document<{}>>({ | ||
path: `/openapi/v3/${path}`, | ||
}) | ||
const paths = Object.keys(sourceDoc.paths) | ||
const resultDoc: OpenAPIV3.Document<{}> = { | ||
...sourceDoc, | ||
paths: {}, | ||
} | ||
|
||
for (const [resourceName, { verbs, namespaces }] of Object.entries(rule)) { | ||
// "/apis/networking.k8s.io/v1/namespaces/{namespace}/ingresses/{name}" | ||
// "/apis/networking.k8s.io/v1/ingresses" | ||
// "/apis/networking.k8s.io/v1/watch/ingresses" | ||
|
||
const namespacedPaths = [ | ||
// operation id: read | replace | delete | patch | ||
// verb: "get" | "list" | "create" | "update" | "patch" | "delete" | ||
`/${path}/namespaces/{namespace}/${resourceName}/{name}`, | ||
// operation id: list | create | deletecollection | ||
// verb: "deletecollection" | "proxy" | "watch" | ||
`/${path}/namespaces/{namespace}/${resourceName}`, | ||
// operation id: connect | ||
// "proxy" | ||
`/${path}/namespaces/{namespace}/${resourceName}/{name}/proxy`, | ||
`/${path}/namespaces/{namespace}/${resourceName}/{name}/proxy/{path}`, | ||
// /watch/はdeprecatedなので、無視します | ||
] | ||
const clusterPath = `/${path}/${resourceName}` | ||
|
||
function isMatchedPath(fullPath: string) { | ||
if (namespaces[0] === '*') { | ||
return fullPath === clusterPath || namespacedPaths.includes(fullPath) | ||
} | ||
|
||
return namespacedPaths.includes(fullPath) | ||
} | ||
paths | ||
.filter((path) => isMatchedPath(path)) | ||
.forEach((path) => { | ||
// "get" | "list" | "watch" | "create" | "update" | "patch" | "delete" | "*" | ||
// "operationId": "replaceEventsV1NamespacedEvent", | ||
if (verbs[0] === '*') { | ||
resultDoc.paths[path] = sourceDoc.paths[path] | ||
} else { | ||
const httpMethods = verbs.map((verb) => | ||
mapK8sVerbToHttpMethod(verb) | ||
) | ||
httpMethods.forEach((httpMethod) => { | ||
if (sourceDoc.paths[path]?.[httpMethod]) { | ||
if (!resultDoc.paths[path]) { | ||
resultDoc.paths[path] = {} | ||
} | ||
const pathsObject = resultDoc.paths[path] | ||
assertNotNull(pathsObject) | ||
pathsObject[httpMethod] = sourceDoc.paths[path]?.[httpMethod] | ||
if (sourceDoc.paths[path]?.parameters) { | ||
pathsObject.parameters = sourceDoc.paths[path]?.parameters | ||
} | ||
} | ||
}) | ||
} | ||
}) | ||
} | ||
docs.push(resultDoc) | ||
} | ||
|
||
const tasks: Promise<void>[] = [] | ||
|
||
if (resourceRules['']) { | ||
const rule = resourceRules[''] | ||
if (!sourcePaths.find((path) => path === 'api/v1')) { | ||
throw Error('The coreV1 API was not found. This is an unexpected state.') | ||
} | ||
|
||
tasks.push(addDoc('api/v1', rule)) | ||
delete resourceRules[''] | ||
} | ||
|
||
for (const [apiGroup, rule] of Object.entries(resourceRules)) { | ||
const matchedApiGroups = sourcePaths.filter((path) => | ||
path.startsWith(`apis/${apiGroup}/`) | ||
) | ||
if (matchedApiGroups.length === 0) { | ||
throw Error( | ||
`The API group '${apiGroup}' is not installed on the k8s cluster. Have you forgotten to install the CRD?` | ||
) | ||
} | ||
matchedApiGroups.forEach((sourcePath) => | ||
tasks.push(addDoc(sourcePath, rule)) | ||
) | ||
} | ||
|
||
await Promise.all(tasks) | ||
|
||
let mergedSwaggerFile = merge({}, ...docs) | ||
|
||
for (const patchFunction of patchFunctions) { | ||
mergedSwaggerFile = patchFunction(mergedSwaggerFile) | ||
} | ||
|
||
const swaggerFilePath = path.join(config.outputFile || cwd, 'swagger.json') | ||
|
||
try { | ||
fs.writeFile( | ||
path.resolve(process.cwd(), config.outputFile), | ||
JSON.stringify(mergedSwaggerFile, undefined, 2) | ||
) | ||
} catch (e) { | ||
console.error(`Failed to write file: ${swaggerFilePath}`, e) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export type ConfigFile = { | ||
kind: 'ServiceAccount' | 'User' | 'Group' | ||
name: string | ||
namespace: string | ||
outputFile: string | ||
} |
Oops, something went wrong.