Skip to content

Commit

Permalink
feat: automate self-generation of swagger for k8s-client and fix clus…
Browse files Browse the repository at this point in the history
…ter-level resource read API generation

- Implement self-generation of swagger specifications for the k8s-client to ensure up-to-date API compatibility.
- Fix an issue where the read API for cluster-level resources was not being generated, enhancing API completeness.
  • Loading branch information
kahirokunn committed May 13, 2024
1 parent 29a11d7 commit b275981
Show file tree
Hide file tree
Showing 14 changed files with 10,412 additions and 17,268 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ConfigFile } from '@kubekit/codegen'

const config: ConfigFile = {
schemaFile: '../openapi/api/v1/swagger.json',
schemaFile: '../generated-openapi.json',
apiFile: '@kubekit/client',
outputFile: '../src/k8s-client/v1.ts',
outputFile: '../src/k8s-client.ts',
}

export default config
10 changes: 10 additions & 0 deletions packages/kubekit-prepare/gen-config/openapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ConfigFile } from '..'

const config: ConfigFile = {
kind: 'ServiceAccount',
name: 'kubekit-prepare',
namespace: 'default',
outputFile: '../generated-openapi.json',
}

export default config
9 changes: 0 additions & 9 deletions packages/kubekit-prepare/gen-config/rbac-v1.ts

This file was deleted.

48 changes: 48 additions & 0 deletions packages/kubekit-prepare/gen-config/sa.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: kubekit-prepare
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kubekit-prepare
namespace: default
rules:
# for validation service account exists
- apiGroups:
- ''
resources:
- serviceaccounts
verbs:
- get
# for aggregate target service account rules
- apiGroups:
- rbac.authorization.k8s.io
resources:
- clusterroles
- roles
verbs:
- get
- list
- apiGroups:
- rbac.authorization.k8s.io
resources:
- rolebindings
- clusterrolebindings
verbs:
- list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kubekit-prepare-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kubekit-prepare
subjects:
- kind: ServiceAccount
name: kubekit-prepare
namespace: default
9,512 changes: 9,512 additions & 0 deletions packages/kubekit-prepare/generated-openapi.json

Large diffs are not rendered by default.

5 changes: 2 additions & 3 deletions packages/kubekit-prepare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"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",
"gen-openapi": "kubectl apply -f gen-config/sa.yaml && yarn build; chmod +x lib/bin/cli.js; yarn cli ./gen-config/openapi.ts",
"gen-k8s-client": "npx @kubekit/codegen gen-config/k8s-client.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",
Expand All @@ -35,7 +35,6 @@
},
"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",
Expand Down
31 changes: 21 additions & 10 deletions packages/kubekit-prepare/src/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ import {
import { HttpMethods, assertNotNull } from '../lib'
import { apiClient } from '@kubekit/client'
import { ConfigFile } from '../config'
import { readCoreV1NamespacedServiceAccount } from '../k8s-client/v1'
import { cleanOpenAPISchema } from '../cleanOpenAPISchema'
import { prettify } from '../prettier'
import { readCoreV1NamespacedServiceAccount } from '../k8s-client'
import { cleanOpenAPISchema } from '../utils/openapi/removeUnusedSchema'
import { prettify } from '../utils/openapi/prettier'

let ts = false
try {
Expand Down Expand Up @@ -92,6 +92,13 @@ async function generateOpenApi(config: ConfigFile) {
apiClient<string>({ path: '/openapi/v3' }),
])

if (process.env.DEBUG) {
console.debug(
'[DEBUG] resourceRules: ',
JSON.stringify(resourceRules, null, 2)
)
}

const source: OpenAPIV3.Document<{}> = JSON.parse(rootOpenApiText)

const cwd = process.cwd()
Expand Down Expand Up @@ -123,25 +130,25 @@ async function generateOpenApi(config: ConfigFile) {
const namespacedPaths: string[] = [
...(subResourceName
? [
`^/${path}/namespaces/{namespace}/${resourceName}/{name}/${subResourceName}$`,
`^/${path}/namespaces/{namespace}/${resourceName}/{name}/${subResourceName}/{.+}$`,
`^/${path}/namespaces/{.+}/${resourceName}/{.+}/${subResourceName}$`,
`^/${path}/namespaces/{.+}/${resourceName}/{.+}/${subResourceName}/{.+}$`,
]
: [
// operation id: read | replace | delete | patch
// verb: "get" | "list" | "create" | "update" | "patch" | "delete"
`^/${path}/namespaces/{namespace}/${resourceName}/{name}$`,
`^/${path}/namespaces/{.+}/${resourceName}/{.+}$`,
// operation id: list | create | deletecollection
// verb: "deletecollection" | "proxy" | "watch"
`^/${path}/namespaces/{namespace}/${resourceName}$`,
`^/${path}/namespaces/{.+}/${resourceName}$`,
]),
// /watch/はdeprecatedなので、無視します
]
const clusterPaths = subResourceName
? [
`^/${path}/${resourceName}$/{name}/${subResourceName}$`,
`^/${path}/${resourceName}$/{name}/${subResourceName}/{.+}$`,
`^/${path}/${resourceName}$/{.+}/${subResourceName}$`,
`^/${path}/${resourceName}$/{.+}/${subResourceName}/{.+}$`,
]
: [`^/${path}/${resourceName}$`]
: [`^/${path}/${resourceName}/{.+}$`, `^/${path}/${resourceName}$`]

function isMatchedPath(fullPath: string): boolean {
if (namespaces[0] === '*') {
Expand All @@ -161,6 +168,10 @@ async function generateOpenApi(config: ConfigFile) {
) !== -1
)
}

if (process.env.DEBUG) {
console.log('[DEBUG] paths:', JSON.stringify(paths, null, 2))
}
paths
.filter((path) => isMatchedPath(path))
.forEach((path) => {
Expand Down
79 changes: 0 additions & 79 deletions packages/kubekit-prepare/src/cleanOpenAPISchema.ts

This file was deleted.

34 changes: 17 additions & 17 deletions packages/kubekit-prepare/src/getRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import {
listRbacAuthorizationV1RoleBindingForAllNamespaces,
readRbacAuthorizationV1ClusterRole,
readRbacAuthorizationV1NamespacedRole,
} from './k8s-client/rbac-v1'
} from './k8s-client'

type Url = string

export async function inspectRules(
kind: string,
name: string,
namespace: string,
namespace: string
) {
const resourceRules = {} as ResourceRules
const nonResourceRules = {} as NonResourceRules
Expand All @@ -32,15 +32,15 @@ export async function inspectRules(

async function getRoles(kind: string, name: string, namespace: string) {
const roleBindings = await listRbacAuthorizationV1RoleBindingForAllNamespaces(
{},
{}
)

const subjectRoles = roleBindings.items
.filter(
(b) =>
b.roleRef.apiGroup === 'rbac.authorization.k8s.io' &&
b.roleRef.kind === 'Role' &&
b.subjects,
b.subjects
)
.map((b) => ({
...b,
Expand All @@ -59,8 +59,8 @@ async function getRoles(kind: string, name: string, namespace: string) {
(subject) =>
subject.kind === kind &&
subject.name === name &&
subject.namespace === namespace,
) !== -1,
subject.namespace === namespace
) !== -1
)
.map((b) => b.roleRef)

Expand All @@ -72,26 +72,26 @@ async function getRoles(kind: string, name: string, namespace: string) {
namespace,
}).catch(() => {
console.warn(
`[WARN] Role name: ${name} namespace: ${namespace} does not exists`,
`[WARN] Role name: ${name} namespace: ${namespace} does not exists`
)
}),
})
)
.filter((v) => v) as IoK8SApiRbacV1Role[],
.filter((v) => v) as IoK8SApiRbacV1Role[]
)
}

// TODO: Support aggregationRule
async function getClusterRoles(kind: string, name: string, namespace: string) {
const clusterRoleBindings = await listRbacAuthorizationV1ClusterRoleBinding(
{},
{}
)

const subjectClusterRoles = clusterRoleBindings.items
.filter(
(b) =>
b.roleRef.apiGroup === 'rbac.authorization.k8s.io' &&
b.roleRef.kind === 'ClusterRole' &&
b.subjects,
b.subjects
)
.map((b) => ({
...b,
Expand All @@ -106,8 +106,8 @@ async function getClusterRoles(kind: string, name: string, namespace: string) {
(subject) =>
subject.kind === kind &&
subject.name === name &&
subject.namespace === namespace,
) !== -1,
subject.namespace === namespace
) !== -1
)
.map((b) => b.roleRef)
.filter((v) => v)
Expand All @@ -119,9 +119,9 @@ async function getClusterRoles(kind: string, name: string, namespace: string) {
name: subjectClusterRole!.name,
}).catch(() => {
console.warn(`[WARN] ClusterRole: ${name} does not exists`)
}),
})
)
.filter((v) => v) as IoK8SApiRbacV1ClusterRole[],
.filter((v) => v) as IoK8SApiRbacV1ClusterRole[]
)
}

Expand Down Expand Up @@ -155,7 +155,7 @@ export type ResourceVerb =
| 'use'

export function mapResourceVerbToKubernetesAction(
verb: ResourceVerb,
verb: ResourceVerb
): KubernetesAction {
switch (verb) {
case 'create':
Expand Down Expand Up @@ -203,7 +203,7 @@ type NonResourceRules = Record<
function mutateDict(
roles: (IoK8SApiRbacV1Role | IoK8SApiRbacV1ClusterRole)[],
resourceRules: ResourceRules,
nonResourceRules: NonResourceRules,
nonResourceRules: NonResourceRules
) {
const allRules = roles
.filter((role) => role)
Expand Down
Loading

0 comments on commit b275981

Please sign in to comment.