Skip to content

Commit

Permalink
feat!: Update Kubernetes client to v1
Browse files Browse the repository at this point in the history
Updates the K8s client to the recently released v1.0.0.

This resolves all vulnerabilities reported for Foreman by `npm audit`, since
the K8s client v1 switched from the deprecated `request` library to
`node-fetch`.

The most relevant code change is due to the new error classes used by the
client, which no longer seem to expose request headers like before, so we
are able to simplify our request logic quite a bit. However, to avoid future
regressions, the test code is modified to recurse through the errors and
check for anything suspicious.

BREAKING CHANGE: Kubernetes client v1 requires HTTPS for connecting to
the Kubernetes API server. While this should not cause any problems in
practice, as Kubernetes API servers are typically exposed over HTTPS, it
would still break non-standard HTTP-only setups.
  • Loading branch information
meyfa committed Dec 23, 2024
1 parent 02a1741 commit d8805c3
Show file tree
Hide file tree
Showing 9 changed files with 889 additions and 923 deletions.
4 changes: 2 additions & 2 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import k8s from '@kubernetes/client-node'
import type { KubeConfig } from '@kubernetes/client-node'
import { KubernetesApi } from './kubernetes/api.js'
import { cronjobRoute } from './api/cronjob.js'
import { podLogsRoute } from './api/pod-logs.js'
Expand All @@ -25,7 +25,7 @@ import type { BackendConfig } from './backend-config.js'
export type { BackendConfig } from './backend-config.js'
export * from './api/errors.js'

export const backend = (kubeConfig: k8s.KubeConfig, config: BackendConfig): FastifyPluginAsync => async (app) => {
export const backend = (kubeConfig: KubeConfig, config: BackendConfig): FastifyPluginAsync => async (app) => {
// Note: NEVER log the config here, as it contains the admin password!
app.log.info('backend_init')

Expand Down
91 changes: 38 additions & 53 deletions backend/src/kubernetes/api.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,56 @@
import k8s, { HttpError, type V1CronJob, type V1EnvVar, type V1Job, type V1Pod } from '@kubernetes/client-node'
import { ApiException, BatchV1Api, CoreV1Api, type KubeConfig, type V1CronJob, type V1EnvVar, type V1Job, type V1Pod } from '@kubernetes/client-node'
import assert from 'node:assert'
import type { BaseLogger } from 'pino'

export const DEFAULT_NAMESPACE = 'default'

export interface KubernetesApiOptions {
kubeConfig: k8s.KubeConfig
kubeConfig: KubeConfig
}

export class KubernetesApi {
constructor (
private readonly log: BaseLogger,
private readonly coreApi: k8s.CoreV1Api,
private readonly batchApi: k8s.BatchV1Api
private readonly coreApi: CoreV1Api,
private readonly batchApi: BatchV1Api
) {
}

static async create (log: BaseLogger, options: KubernetesApiOptions): Promise<KubernetesApi> {
const kubeConfig = options.kubeConfig
const coreApi = kubeConfig.makeApiClient(k8s.CoreV1Api)
const batchApi = kubeConfig.makeApiClient(k8s.BatchV1Api)
const coreApi = kubeConfig.makeApiClient(CoreV1Api)
const batchApi = kubeConfig.makeApiClient(BatchV1Api)
return new KubernetesApi(log, coreApi, batchApi)
}

private async request <T> (fn: () => Promise<T>): Promise<T> {
// The Kubernetes client throws HTTP errors that contain the request and response headers.
// This is a security risk if the error is logged, so we catch and rethrow the error without these objects.
try {
return await fn()
} catch (err) {
if (err instanceof HttpError) {
// Force TypeScript to cause an error if the response property is missing
((err satisfies { response: object }) as any).response = undefined
}
throw err
}
}

async getCronJob (options: {
namespace: string
name: string
}): Promise<V1CronJob | undefined> {
this.log.debug({ options }, 'k8s_getCronJob')
return await this.request(async () => {
try {
const result = await this.batchApi.readNamespacedCronJob(options.name, options.namespace)
return result.body
} catch (err) {
if (err instanceof HttpError && err.statusCode === 404) {
this.log.warn({ options }, 'k8s_getCronJob: not found')
return undefined
}
throw err
try {
return await this.batchApi.readNamespacedCronJob({
namespace: options.namespace,
name: options.name
})
} catch (err) {
if (err instanceof ApiException && err.code === 404) {
this.log.warn({ options }, 'k8s_getCronJob: not found')
return undefined
}
})
throw err
}
}

async getJobs (options: {
namespace: string
}): Promise<V1Job[] | undefined> {
this.log.debug({ options }, 'k8s_getJobs')
const result = await this.request(async () => {
return await this.batchApi.listNamespacedJob(options.namespace)
})
return result.body.items.map((item) => ({
const result = await this.batchApi.listNamespacedJob(options)
return result.items.map((item) => ({
...item,
kind: result.body.kind?.replace(/List$/, ''),
apiVersion: result.body.apiVersion
kind: result.kind?.replace(/List$/, ''),
apiVersion: result.apiVersion
}))
}

Expand All @@ -76,13 +60,14 @@ export class KubernetesApi {
}): Promise<V1Pod[]> {
this.log.debug({ options }, 'k8s_getPodsForJob')
const labelSelector = `job-name=${options.name}`
const result = await this.request(async () => {
return await this.coreApi.listNamespacedPod(options.namespace, undefined, undefined, undefined, undefined, labelSelector)
const result = await this.coreApi.listNamespacedPod({
namespace: options.namespace,
labelSelector
})
return result.body.items.map((item) => ({
return result.items.map((item) => ({
...item,
kind: result.body.kind?.replace(/List$/, ''),
apiVersion: result.body.apiVersion
kind: result.kind?.replace(/List$/, ''),
apiVersion: result.apiVersion
}))
}

Expand All @@ -91,10 +76,10 @@ export class KubernetesApi {
name: string
}): Promise<string> {
this.log.debug({ options }, 'k8s_getPodLogs')
const result = await this.request(async () => {
return await this.coreApi.readNamespacedPodLog(options.name, options.namespace)
return await this.coreApi.readNamespacedPodLog({
namespace: options.namespace,
name: options.name
})
return result.body
}

async triggerCronJob (options: {
Expand Down Expand Up @@ -123,14 +108,14 @@ export class KubernetesApi {
jobBody = applyEnvToJobContainers(jobBody, options.env)
// Cleanup job after 1 hour
jobBody = applyTtl(jobBody, 60 * 60)
const result = await this.request(async () => {
return await this.batchApi.createNamespacedJob(namespace, jobBody)
return await this.batchApi.createNamespacedJob({
namespace,
body: jobBody
})
return result.body
}
}

function applyMetadataAnnotations (jobBody: k8s.V1Job, annotations: Record<string, string>): k8s.V1Job {
function applyMetadataAnnotations (jobBody: V1Job, annotations: Record<string, string>): V1Job {
return {
...jobBody,
metadata: {
Expand All @@ -143,7 +128,7 @@ function applyMetadataAnnotations (jobBody: k8s.V1Job, annotations: Record<strin
}
}

function applyMetadataLabels (jobBody: k8s.V1Job, labels: Record<string, string>): k8s.V1Job {
function applyMetadataLabels (jobBody: V1Job, labels: Record<string, string>): V1Job {
return {
...jobBody,
metadata: {
Expand All @@ -156,7 +141,7 @@ function applyMetadataLabels (jobBody: k8s.V1Job, labels: Record<string, string>
}
}

function applyMetadataName (jobBody: k8s.V1Job, name: string): k8s.V1Job {
function applyMetadataName (jobBody: V1Job, name: string): V1Job {
return {
...jobBody,
metadata: {
Expand All @@ -166,7 +151,7 @@ function applyMetadataName (jobBody: k8s.V1Job, name: string): k8s.V1Job {
}
}

function applyEnvToJobContainers (jobBody: k8s.V1Job, env: Record<string, string>): k8s.V1Job {
function applyEnvToJobContainers (jobBody: V1Job, env: Record<string, string>): V1Job {
if (jobBody.spec?.template.spec?.containers == null) {
// No containers to apply env to
return jobBody
Expand All @@ -189,7 +174,7 @@ function applyEnvToJobContainers (jobBody: k8s.V1Job, env: Record<string, string
}
}

function applyTtl (jobBody: k8s.V1Job, ttlSecondsAfterFinished: number): k8s.V1Job {
function applyTtl (jobBody: V1Job, ttlSecondsAfterFinished: number): V1Job {
if (jobBody.spec == null) {
return jobBody
}
Expand Down
30 changes: 30 additions & 0 deletions backend/test/fixtures/cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFJzCCAw+gAwIBAgIUYrtORXAcGYbErr0yhOBR3OmM/2wwDQYJKoZIhvcNAQEL
BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI0MTIyMzEwNTc0MVoYDzIxMjQx
MTI5MTA1NzQxWjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQDJSeoJV7JW+Z3j9q5FZbSNc180CXotXhLdO72O5iam
SEo7+EEtcYcjOtrzyITesEMAD9FvvAHsyDSWjFWZecNnnOkYR3JLAWqeLOcCHXku
0nsPSRVEhyfj2tyWxg8cI3yzrbWaQgx3JitfwRJYmhqBeZ7ardJ+mCUoMSGKcDvH
fEs/b8HCqmY486Vz4NUSeQzE5Jxf1kfWTdf/C3wIxjo/XDGsJjPmOnCdajDNjaP9
1Y8dY0ZLRA5BVFwwQietJLx1PtM0W7YCiWWrmY2Cv+H1zATOrkMYNyN1JXmnsMMQ
cQyLBg4CIFixl//ZRPShu15n40VeBnmALMcDvH9pEYBFLUVipem5GDcAWKYFOrG1
Q8vGgVon8dtzV1azMbvukiWwLJpilAFKADpVWzMtRqIkUw1HTUJdDp79zHCv0AUK
PEGJyczWda5KFKYJJiPRawOhSnOpiEeWZLJXO6CkzNTfUNZ+cT2VyRXaX+g4uT+H
e6zGlO9aMGeylDNvalCj5RUtjahCS11vlhcy5rX3fGiDX3sm3lfsY1tv3FxPnsAB
lICQX2TyZnC6CngfsqVv1/XqqrrzwdpMaINtdkeFXQA2vZxGDjEQxJuPCGODlCnk
GA+DQWaF3XmBVLjGWoiL5fVCBggdEp+t0LoCi4h6FIi2GxxiEC65bLbWXqNV1lJn
VwIDAQABo28wbTAdBgNVHQ4EFgQUevuzE5zqCZjYb4SGj+ig2Cefq3swHwYDVR0j
BBgwFoAUevuzE5zqCZjYb4SGj+ig2Cefq3swDwYDVR0TAQH/BAUwAwEB/zAaBgNV
HREEEzARgglsb2NhbGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBAGA3rTla
PNyB+NxKDjB+U8fT8Gj+0tVyWGEVwVCS51ILvVSm40gVA59QfeR75IJqD4ve6L07
nvSSH6oNFkAEUZaNcdMMEaQus59Wz+patoxaPfuOTDJe1krdzfZ+8fKWrC4c2I1t
TPLHPE1/0glasYZ+dWgQvhFewo29Kx5WsbC46d/3aEw0wpJGxJNutv1UOJS8MIRF
WkfN/GhZr/h5M4H1+RqysNwNdQZwC8vKTGQ4+eUs37H6XQlpAhoIuXJMiHE3++P2
aS0DHj8ADGclHJeSMtOVmA+tJvcRHkcTEWts2kWjpBcRn/OSwcLWDKLvvkJK13Ou
hafjpsTuc8/rv000Gjr7FalFurhPRA2SZG/BwErwCbRXU8Mrrrq6UTHZ/d6cbg7O
DiqgAn1dikmSq7tzsdS2164ipGPLifrlJ32VZnT+7Z7fZeCu8eFgSK8ExQtzgA1Z
nIAnGkEXkj0yFNlF87UgZOeyZTIumT7hEsSvRTSXCLFFzlUxmh9Ni4Am7urOTntO
WYdgp9MuAWpIYn89gwG4ZDmLlwvg2oR/fvj8BQM2y19FCMHBbe7U177x9fsvp+7p
yf6O0O72wZT5fspmF+V4qNNu6vwQHELIiYt9jwZszTvksB62PZmz8j04F8ic/mv6
ffpuThqtUaSzNeiJnCLNEThecEvevMmFtzJa
-----END CERTIFICATE-----
52 changes: 52 additions & 0 deletions backend/test/fixtures/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
qmUhZ4HQ/bq8TW3ik8Nu7YNstEgArWA=
-----END PRIVATE KEY-----
Loading

0 comments on commit d8805c3

Please sign in to comment.