UNPKG

kubernetes-fluent-client

Version:

A @kubernetes/client-node fluent API wrapper that leverages K8s Server Side Apply.

317 lines (278 loc) 8.68 kB
// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors import { KubernetesListObject, KubernetesObject } from "@kubernetes/client-node"; import { Operation } from "fast-json-patch"; import { StatusCodes } from "http-status-codes"; import type { PartialDeep } from "type-fest"; import { fetch } from "../fetch.js"; import { modelToGroupVersionKind } from "../kinds.js"; import { GenericClass } from "../types.js"; import { K8sInit, Paths } from "./types.js"; import { Filters, WatchAction, FetchMethods, ApplyCfg } from "./shared-types.js"; import { k8sCfg, k8sExec } from "./utils.js"; import { WatchCfg, Watcher } from "./watch.js"; import { hasLogs } from "../helpers.js"; import { Pod, type Service, type ReplicaSet } from "../upstream.js"; /** * Kubernetes fluent API inspired by Kubectl. Pass in a model, then call filters and actions on it. * * @param model - the model to use for the API * @param filters - (optional) filter overrides, can also be chained * @returns a fluent API for the model */ export function K8s<T extends GenericClass, K extends KubernetesObject = InstanceType<T>>( model: T, filters: Filters = {}, ): K8sInit<T, K> { const withFilters = { WithField, WithLabel, Get, Delete, Evict, Watch, Logs }; const matchedKind = filters.kindOverride || modelToGroupVersionKind(model.name); /** * @inheritdoc * @see {@link K8sInit.InNamespace} */ function InNamespace(namespace: string) { if (filters.namespace) { throw new Error(`Namespace already specified: ${filters.namespace}`); } filters.namespace = namespace; return withFilters; } /** * @inheritdoc * @see {@link K8sInit.WithField} */ function WithField<P extends Paths<K>>(key: P, value: string) { filters.fields = filters.fields || {}; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore filters.fields[key] = value; return withFilters; } /** * @inheritdoc * @see {@link K8sInit.WithLabel} */ function WithLabel(key: string, value = "") { filters.labels = filters.labels || {}; filters.labels[key] = value; return withFilters; } /** * Sync the filters with the provided payload. * * @param payload - the payload to sync with */ function syncFilters(payload: K) { // Ensure the payload has metadata payload.metadata = payload.metadata || {}; if (!filters.namespace) { filters.namespace = payload.metadata.namespace; } if (!filters.name) { filters.name = payload.metadata.name; } if (!payload.apiVersion) { payload.apiVersion = [matchedKind.group, matchedKind.version].filter(Boolean).join("/"); } if (!payload.kind) { payload.kind = matchedKind.kind; } } async function Logs(name?: string): Promise<string[]>; /** * @inheritdoc * @see {@link K8sInit.Logs} */ async function Logs(name?: string): Promise<string[]> { let labels: Record<string, string> = {}; const { kind } = matchedKind; const { namespace } = filters; const podList: K[] = []; if (name) { if (filters.name) { throw new Error(`Name already specified: ${filters.name}`); } filters.name = name; } if (!namespace) { throw new Error("Namespace must be defined"); } if (!hasLogs(kind)) { throw new Error("Kind must be Pod or have a selector"); } try { const object = await k8sExec<T, K>(model, filters, { method: FetchMethods.GET }); if (kind !== "Pod") { if (kind === "Service") { const svc: InstanceType<typeof Service> = object; labels = svc.spec!.selector ?? {}; } else if ( kind === "ReplicaSet" || kind === "Deployment" || kind === "StatefulSet" || kind === "DaemonSet" ) { const rs: InstanceType<typeof ReplicaSet> = object; labels = rs.spec!.selector.matchLabels ?? {}; } const list = await K8s(Pod, { namespace: filters.namespace, labels }).Get(); list.items.forEach(item => { return podList.push(item as unknown as K); }); } else { podList.push(object); } } catch { throw new Error(`Failed to get logs in KFC Logs function`); } const podModel = { ...model, name: "V1Pod" }; const logPromises = podList.map(po => k8sExec<T, string>( podModel, { ...filters, name: po.metadata!.name! }, { method: FetchMethods.LOG }, ), ); const responses = await Promise.all(logPromises); const combinedString = responses.reduce( (accumulator: string[], currentString: string, i: number) => { const prefixedLines = currentString .split("\n") .map(line => { return line !== "" ? `[pod/${podList[i].metadata!.name!}] ${line}` : ""; }) .filter(str => str !== ""); return [...accumulator, ...prefixedLines]; }, [], ); return combinedString; } async function Get(): Promise<KubernetesListObject<K>>; async function Get(name: string): Promise<K>; /** * @inheritdoc * @see {@link K8sInit.Get} */ async function Get(name?: string) { if (name) { if (filters.name) { throw new Error(`Name already specified: ${filters.name}`); } filters.name = name; } return k8sExec<T, K | KubernetesListObject<K>>(model, filters, { method: FetchMethods.GET }); } /** * @inheritdoc * @see {@link K8sInit.Delete} */ async function Delete(filter?: K | string): Promise<void> { if (typeof filter === "string") { filters.name = filter; } else if (filter) { syncFilters(filter); } try { // Try to delete the resource await k8sExec<T, void>(model, filters, { method: FetchMethods.DELETE }); } catch (e) { // If the resource doesn't exist, ignore the error if (e.status === StatusCodes.NOT_FOUND) { return; } throw e; } } /** * @inheritdoc * @see {@link K8sInit.Apply} */ async function Apply( resource: PartialDeep<K>, applyCfg: ApplyCfg = { force: false }, ): Promise<K> { syncFilters(resource as K); return k8sExec(model, filters, { method: FetchMethods.APPLY, payload: resource }, applyCfg); } /** * @inheritdoc * @see {@link K8sInit.Create} */ async function Create(resource: K): Promise<K> { syncFilters(resource); return k8sExec(model, filters, { method: FetchMethods.POST, payload: resource }); } /** * @inheritdoc * @see {@link K8sInit.Evict} */ async function Evict(filter?: K | string): Promise<void> { if (typeof filter === "string") { filters.name = filter; } else if (filter) { syncFilters(filter); } try { const evictionPayload = { apiVersion: "policy/v1", kind: "Eviction", metadata: { name: filters.name, namespace: filters.namespace, }, }; // Try to evict the resource await k8sExec<T, void>(model, filters, { method: FetchMethods.POST, payload: evictionPayload, }); } catch (e) { // If the resource doesn't exist, ignore the error if (e.status === StatusCodes.NOT_FOUND) { return; } throw e; } } /** * @inheritdoc * @see {@link K8sInit.Patch} */ async function Patch(payload: Operation[]): Promise<K> { // If there are no operations, throw an error if (payload.length < 1) { throw new Error("No operations specified"); } return k8sExec(model, filters, { method: FetchMethods.PATCH, payload }); } /** * @inheritdoc * @see {@link K8sInit.PatchStatus} */ async function PatchStatus(resource: PartialDeep<K>): Promise<K> { syncFilters(resource as K); return k8sExec(model, filters, { method: FetchMethods.PATCH_STATUS, payload: resource }); } /** * @inheritdoc * @see {@link K8sInit.Watch} */ function Watch(callback: WatchAction<T>, watchCfg?: WatchCfg) { return new Watcher(model, filters, callback, watchCfg); } /** * @inheritdoc * @see {@link K8sInit.Raw} */ async function Raw(url: string, method: FetchMethods = FetchMethods.GET) { const thing = await k8sCfg(method); const { opts, serverUrl } = thing; const resp = await fetch<K>(`${serverUrl}${url}`, opts); if (resp.ok) { return resp.data; } throw resp; } return { InNamespace, Apply, Create, Patch, PatchStatus, Raw, ...withFilters }; }