Skip to content

Commit 0dcea1b

Browse files
authored
Merge pull request #56 from PRO-Robotech/feature/dev
kube clients | events endpoints
2 parents 344a041 + 9b4c8d9 commit 0dcea1b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1114
-98
lines changed

package-lock.json

Lines changed: 535 additions & 77 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"swagger:gen": "ts-node ./src/swagger.ts"
1515
},
1616
"dependencies": {
17+
"@kubernetes/client-node": "1.4.0",
1718
"@readme/openapi-parser": "4.0.0",
1819
"apicache": "1.6.3",
1920
"axios": "1.10.0",

src/constants/envs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import dotenv from 'dotenv'
22

33
dotenv.config()
44

5-
export const DEV_KUBE_API_URL = process.env.DEV_KUBE_API_URL
5+
export const DEV_KUBE_API_URL = process.env.DEV_KUBE_API_URL || 'no-dev-kube-api-url'
66
export const BASE_API_GROUP = process.env.BASE_API_GROUP
77
export const BASE_API_VERSION = process.env.BASE_API_VERSION
88
export const BASEPREFIX = process.env.BASEPREFIX || ''

src/constants/kubeClients.ts

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
// kube-client.ts
2+
import fs from 'fs'
3+
import path from 'path'
4+
import {
5+
KubeConfig,
6+
// Core and groups
7+
CoreV1Api,
8+
AppsV1Api,
9+
BatchV1Api,
10+
AutoscalingV1Api,
11+
AutoscalingV2Api,
12+
NetworkingV1Api,
13+
RbacAuthorizationV1Api,
14+
PolicyV1Api,
15+
SchedulingV1Api,
16+
StorageV1Api,
17+
CertificatesV1Api,
18+
CoordinationV1Api,
19+
EventsV1Api,
20+
AuthenticationV1Api,
21+
AuthorizationV1Api,
22+
DiscoveryV1Api,
23+
ApiregistrationV1Api,
24+
ApiextensionsV1Api,
25+
AdmissionregistrationV1Api,
26+
// Misc / utility
27+
VersionApi,
28+
CustomObjectsApi,
29+
// Types for discovery payloads
30+
V1APIGroupList,
31+
V1APIVersions,
32+
V1APIResourceList,
33+
} from '@kubernetes/client-node'
34+
import { KUBE_API_URL, DEV_KUBE_API_URL, DEVELOPMENT } from './envs'
35+
36+
type TKubeClientsSurface = {
37+
// Core
38+
core: CoreV1Api
39+
apps: AppsV1Api
40+
41+
// Workloads / scheduling / controllers
42+
batch: BatchV1Api
43+
autoscalingV1: AutoscalingV1Api
44+
autoscalingV2: AutoscalingV2Api
45+
scheduling: SchedulingV1Api
46+
47+
// Networking / policy / RBAC
48+
networking: NetworkingV1Api
49+
policy: PolicyV1Api
50+
rbac: RbacAuthorizationV1Api
51+
52+
// Platform infra
53+
storage: StorageV1Api
54+
certificates: CertificatesV1Api
55+
coordination: CoordinationV1Api
56+
events: EventsV1Api
57+
58+
// AuthN/Z
59+
authentication: AuthenticationV1Api
60+
authorization: AuthorizationV1Api
61+
62+
// Discovery / registration / extensions / admission
63+
discovery: DiscoveryV1Api
64+
apiregistration: ApiregistrationV1Api
65+
apiextensions: ApiextensionsV1Api
66+
admissionregistration: AdmissionregistrationV1Api
67+
68+
// Version info
69+
version: VersionApi
70+
71+
// CRDs
72+
customObjects: CustomObjectsApi
73+
}
74+
75+
type TDiscoveryHelpers = {
76+
getApiGroups: (signal?: AbortSignal) => Promise<V1APIGroupList>
77+
getCoreApiVersions: (signal?: AbortSignal) => Promise<V1APIVersions>
78+
getResourcesFor: (group: string, version: string, signal?: AbortSignal) => Promise<V1APIResourceList>
79+
}
80+
81+
/**
82+
* Preserve original in-cluster file logic + logs
83+
*/
84+
const serviceAccountDir = '/var/run/secrets/kubernetes.io/serviceaccount'
85+
const caPath = path.join(serviceAccountDir, 'ca.crt')
86+
const tokenPath = path.join(serviceAccountDir, 'token')
87+
88+
let ca: Buffer | undefined
89+
if (fs.existsSync(caPath)) {
90+
ca = fs.readFileSync(caPath)
91+
console.log('✅ Using incluster CA')
92+
}
93+
94+
let bearerToken: string | undefined
95+
if (fs.existsSync(tokenPath)) {
96+
bearerToken = fs.readFileSync(tokenPath, 'utf8').trim()
97+
console.log('✅ Using incluster ServiceAccount token')
98+
}
99+
100+
export const baseUrl = DEVELOPMENT ? DEV_KUBE_API_URL : KUBE_API_URL
101+
102+
const buildCluster = () => {
103+
return DEVELOPMENT
104+
? { name: 'cluster', server: baseUrl, skipTLSVerify: true }
105+
: {
106+
name: 'cluster',
107+
server: baseUrl,
108+
caData: ca ? ca.toString('base64') : undefined,
109+
skipTLSVerify: false,
110+
}
111+
}
112+
113+
/** ---------- Admin (SA) client: like old kubeApi ---------- */
114+
const buildSaKubeConfig = (): KubeConfig => {
115+
const kc = new KubeConfig()
116+
const cluster = buildCluster()
117+
118+
kc.loadFromOptions({
119+
clusters: [cluster],
120+
users: [
121+
// SA auth ONLY here
122+
DEVELOPMENT ? { name: 'dev', token: undefined } : { name: 'sa', token: bearerToken },
123+
],
124+
contexts: [{ name: 'ctx', user: DEVELOPMENT ? 'dev' : 'sa', cluster: 'cluster' }],
125+
currentContext: 'ctx',
126+
})
127+
128+
return kc
129+
}
130+
131+
const kcSa = buildSaKubeConfig()
132+
133+
export const kubeCore: CoreV1Api = kcSa.makeApiClient(CoreV1Api)
134+
export const kubeApps: AppsV1Api = kcSa.makeApiClient(AppsV1Api)
135+
136+
export const allApis: TKubeClientsSurface = {
137+
// Core
138+
core: kubeCore,
139+
apps: kubeApps,
140+
141+
// Workloads / scheduling / controllers
142+
batch: kcSa.makeApiClient(BatchV1Api),
143+
autoscalingV1: kcSa.makeApiClient(AutoscalingV1Api),
144+
autoscalingV2: kcSa.makeApiClient(AutoscalingV2Api),
145+
scheduling: kcSa.makeApiClient(SchedulingV1Api),
146+
147+
// Networking / policy / RBAC
148+
networking: kcSa.makeApiClient(NetworkingV1Api),
149+
policy: kcSa.makeApiClient(PolicyV1Api),
150+
rbac: kcSa.makeApiClient(RbacAuthorizationV1Api),
151+
152+
// Platform infra
153+
storage: kcSa.makeApiClient(StorageV1Api),
154+
certificates: kcSa.makeApiClient(CertificatesV1Api),
155+
coordination: kcSa.makeApiClient(CoordinationV1Api),
156+
events: kcSa.makeApiClient(EventsV1Api),
157+
158+
// AuthN/Z
159+
authentication: kcSa.makeApiClient(AuthenticationV1Api),
160+
authorization: kcSa.makeApiClient(AuthorizationV1Api),
161+
162+
// Discovery / registration / extensions / admission
163+
discovery: kcSa.makeApiClient(DiscoveryV1Api),
164+
apiregistration: kcSa.makeApiClient(ApiregistrationV1Api),
165+
apiextensions: kcSa.makeApiClient(ApiextensionsV1Api),
166+
admissionregistration: kcSa.makeApiClient(AdmissionregistrationV1Api),
167+
168+
// Version info
169+
version: kcSa.makeApiClient(VersionApi),
170+
171+
// CRDs
172+
customObjects: kcSa.makeApiClient(CustomObjectsApi),
173+
}
174+
175+
/**
176+
* ---- API discovery helpers (v1 everything) ----
177+
* Lightweight wrappers using the underlying client's raw `request` method.
178+
*/
179+
const rawGet = async <T>(client: any, path: string, signal?: AbortSignal): Promise<T> => {
180+
const opts: any = { method: 'GET', uri: `${baseUrl}${path}` }
181+
if (DEVELOPMENT) opts.rejectUnauthorized = false
182+
if (signal) opts.signal = signal
183+
return client.request(opts).then((res: any) => (res.body ? JSON.parse(res.body) : res))
184+
}
185+
186+
/** List non-core API groups (GET /apis) */
187+
export const getApiGroups = async (signal?: AbortSignal): Promise<V1APIGroupList> => {
188+
return rawGet(allApis.core, '/apis', signal)
189+
}
190+
191+
/** List core API versions (GET /api) */
192+
export const getCoreApiVersions = async (signal?: AbortSignal): Promise<V1APIVersions> => {
193+
return rawGet(allApis.core, '/api', signal)
194+
}
195+
196+
/** List resources for a given group/version (GET /apis/{group}/{version}) */
197+
export const getResourcesFor = async (
198+
group: string,
199+
version: string,
200+
signal?: AbortSignal,
201+
): Promise<V1APIResourceList> => {
202+
const path = `/apis/${group}/${version}`
203+
return rawGet(allApis.core, path, signal)
204+
}
205+
206+
/** ---------- User-proxied client: STRICTLY no SA auth ---------- */
207+
export const createUserKubeClient = (
208+
userHeaders: Record<string, string | string[] | undefined>,
209+
): TKubeClientsSurface &
210+
TDiscoveryHelpers & {
211+
kubeConfig: KubeConfig // <-- expose KC for Watch
212+
request: (opts: any) => any // <-- convenience raw requester (from core client)
213+
} => {
214+
// Build a config with NO credentials at all
215+
const kc = new KubeConfig()
216+
const cluster = buildCluster()
217+
218+
kc.loadFromOptions({
219+
clusters: [cluster as any],
220+
users: [{ name: 'user-proxy' }] as any, // no token, no certs, nothing
221+
contexts: [{ name: 'ctx', user: 'user-proxy', cluster: 'cluster' }],
222+
currentContext: 'ctx',
223+
})
224+
225+
// Make all standard clients
226+
const clients: TKubeClientsSurface = {
227+
core: kc.makeApiClient(CoreV1Api),
228+
apps: kc.makeApiClient(AppsV1Api),
229+
batch: kc.makeApiClient(BatchV1Api),
230+
autoscalingV1: kc.makeApiClient(AutoscalingV1Api),
231+
autoscalingV2: kc.makeApiClient(AutoscalingV2Api),
232+
scheduling: kc.makeApiClient(SchedulingV1Api),
233+
networking: kc.makeApiClient(NetworkingV1Api),
234+
policy: kc.makeApiClient(PolicyV1Api),
235+
rbac: kc.makeApiClient(RbacAuthorizationV1Api),
236+
storage: kc.makeApiClient(StorageV1Api),
237+
certificates: kc.makeApiClient(CertificatesV1Api),
238+
coordination: kc.makeApiClient(CoordinationV1Api),
239+
events: kc.makeApiClient(EventsV1Api),
240+
authentication: kc.makeApiClient(AuthenticationV1Api),
241+
authorization: kc.makeApiClient(AuthorizationV1Api),
242+
discovery: kc.makeApiClient(DiscoveryV1Api),
243+
apiregistration: kc.makeApiClient(ApiregistrationV1Api),
244+
apiextensions: kc.makeApiClient(ApiextensionsV1Api),
245+
admissionregistration: kc.makeApiClient(AdmissionregistrationV1Api),
246+
version: kc.makeApiClient(VersionApi),
247+
customObjects: kc.makeApiClient(CustomObjectsApi),
248+
}
249+
250+
const normalizeHeaders = (h: Record<string, string | string[] | undefined>): Record<string, string> => {
251+
return Object.fromEntries(
252+
Object.entries(h)
253+
.filter(([, v]) => v !== undefined)
254+
.map(([k, v]) => [k, Array.isArray(v) ? v.join(',') : (v as string)]),
255+
)
256+
}
257+
const normalizedHeaders = normalizeHeaders(userHeaders)
258+
259+
const patch = (client: any): void => {
260+
const orig = client.request.bind(client)
261+
262+
client.request = (opts: any) => {
263+
opts.headers = { ...(opts.headers || {}), ...normalizedHeaders }
264+
if (DEVELOPMENT) {
265+
opts.rejectUnauthorized = false
266+
}
267+
return orig(opts)
268+
}
269+
}
270+
271+
Object.values(clients).forEach(patch)
272+
273+
// Provide the same discovery helpers, bound to these user clients
274+
const rawGetUser = async <T>(path: string, signal?: AbortSignal): Promise<T> => {
275+
const opts: any = { method: 'GET', uri: `${baseUrl}${path}` }
276+
if (DEVELOPMENT) opts.rejectUnauthorized = false
277+
if (signal) opts.signal = signal
278+
return (clients.core as any).request(opts).then((res: any) => (res.body ? JSON.parse(res.body) : res))
279+
}
280+
281+
const helpers: TDiscoveryHelpers = {
282+
getApiGroups: (signal?: AbortSignal) => rawGetUser('/apis', signal),
283+
getCoreApiVersions: (signal?: AbortSignal) => rawGetUser('/api', signal),
284+
getResourcesFor: (group: string, version: string, signal?: AbortSignal) =>
285+
rawGetUser(`/apis/${group}/${version}`, signal),
286+
}
287+
288+
// --- SAFE way to expose `request` ---
289+
type TKubeApiWithRequest = {
290+
request: (opts: Record<string, unknown>) => Promise<unknown>
291+
}
292+
293+
const hasRequest = (client: object): client is TKubeApiWithRequest => {
294+
return typeof (client as any).request === 'function'
295+
}
296+
297+
let requestFn: TKubeApiWithRequest['request']
298+
299+
if (hasRequest(clients.core)) {
300+
requestFn = clients.core.request.bind(clients.core)
301+
} else {
302+
console.error('CoreV1Api does not expose a request() method — check @kubernetes/client-node version')
303+
}
304+
305+
const request = (clients.core as unknown as { request: (opts: any) => any }).request.bind(clients.core)
306+
307+
return { ...clients, ...helpers, kubeConfig: kc, request }
308+
}
309+
310+
/**
311+
* ---- Timeout parity (5s “fail fast”) ----
312+
* @kubernetes/client-node doesn’t expose a global client timeout.
313+
* To preserve your 5_000ms behavior, use an AbortController per request.
314+
*
315+
* Usage:
316+
* const { signal, cancel } = requestTimeout(5000)
317+
* await kubeCore.listNamespace(undefined, undefined, undefined, undefined, undefined, undefined, { signal })
318+
* // or pass `signal` as the last options object where applicable
319+
*/
320+
/*
321+
* Usage examples:
322+
* // List pods in "default" with the same 5s timeout you had:
323+
* const { signal } = requestTimeout(5_000)
324+
* const pods = await kubeCore.listNamespacedPod({ namespace: 'default' }, { signal })
325+
*
326+
* // Using the “user” client (no default auth), also with timeout:
327+
* const { signal: sig2 } = requestTimeout()
328+
* const ns = await userCore.listNamespace({}, { sig2 })
329+
*/
330+
export const requestTimeout = (
331+
ms = 5_000,
332+
): {
333+
signal: AbortSignal
334+
cancel: () => void
335+
} => {
336+
const controller = new AbortController()
337+
const t = setTimeout(() => controller.abort(), ms)
338+
// give caller a way to cancel early if needed
339+
const cancel = () => clearTimeout(t)
340+
return { signal: controller.signal, cancel }
341+
}

0 commit comments

Comments
 (0)