Skip to content

Commit

Permalink
feat: initialize @kubekit/prepare project for generating Kubernetes O…
Browse files Browse the repository at this point in the history
…penAPI specs based on ServiceAccount permissions
  • Loading branch information
kahirokunn committed May 9, 2024
1 parent 617edc3 commit e4af319
Show file tree
Hide file tree
Showing 20 changed files with 19,393 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,4 @@ eks-blueprints-add-ons
examples/generate-vercel-client/swagger.json
eks-blueprints-add-ons
openapi
packages/kubekit-prepare/lib
1 change: 1 addition & 0 deletions packages/kubekit-prepare/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tmp
38 changes: 38 additions & 0 deletions packages/kubekit-prepare/README.md
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.
9 changes: 9 additions & 0 deletions packages/kubekit-prepare/gen-config/rbac-v1.ts
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
9 changes: 9 additions & 0 deletions packages/kubekit-prepare/gen-config/v1.ts
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
47 changes: 47 additions & 0 deletions packages/kubekit-prepare/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"name": "@kubekit/prepare",
"version": "0.0.1",
"main": "lib/index.js",
"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",
"test": "yarn build; chmod +x lib/bin/cli.js; yarn cli tests/test1.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",
"lodash.merge": "^4.6.2"
}
}
191 changes: 191 additions & 0 deletions packages/kubekit-prepare/src/bin/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#!/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'
import { writeFile } from 'fs'

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]
}
})
}
})
}
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)
}
}
6 changes: 6 additions & 0 deletions packages/kubekit-prepare/src/config.ts
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
}
Loading

0 comments on commit e4af319

Please sign in to comment.