UNPKG

kubernetes-fluent-client

Version:

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

258 lines (257 loc) 8.27 kB
// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors import { StatusCodes } from "http-status-codes"; import { fetch } from "../fetch.js"; import { modelToGroupVersionKind } from "../kinds.js"; import { FetchMethods } from "./shared-types.js"; import { k8sCfg, k8sExec } from "./utils.js"; import { Watcher } from "./watch.js"; import { hasLogs } from "../helpers.js"; import { Pod } 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(model, filters = {}) { const withFilters = { WithField, WithLabel, Get, Delete, Evict, Watch, Logs }; const matchedKind = filters.kindOverride || modelToGroupVersionKind(model.name); /** * @inheritdoc * @see {@link K8sInit.InNamespace} */ function InNamespace(namespace) { if (filters.namespace) { throw new Error(`Namespace already specified: ${filters.namespace}`); } filters.namespace = namespace; return withFilters; } /** * @inheritdoc * @see {@link K8sInit.WithField} */ function WithField(key, value) { 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, 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) { // 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; } } /** * @inheritdoc * @see {@link K8sInit.Logs} */ async function Logs(name) { let labels = {}; const { kind } = matchedKind; const { namespace } = filters; const podList = []; 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(model, filters, FetchMethods.GET); if (kind !== "Pod") { if (kind === "Service") { const svc = object; labels = svc.spec.selector ?? {}; } else if (kind === "ReplicaSet" || kind === "Deployment" || kind === "StatefulSet" || kind === "DaemonSet") { const rs = object; labels = rs.spec.selector.matchLabels ?? {}; } const list = await K8s(Pod, { namespace: filters.namespace, labels }).Get(); list.items.forEach(item => { return podList.push(item); }); } 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(podModel, { ...filters, name: po.metadata.name }, FetchMethods.LOG)); const responses = await Promise.all(logPromises); const combinedString = responses.reduce((accumulator, currentString, i) => { const prefixedLines = currentString .split("\n") .map(line => { return line !== "" ? `[pod/${podList[i].metadata.name}] ${line}` : ""; }) .filter(str => str !== ""); return [...accumulator, ...prefixedLines]; }, []); return combinedString; } /** * @inheritdoc * @see {@link K8sInit.Get} */ async function Get(name) { if (name) { if (filters.name) { throw new Error(`Name already specified: ${filters.name}`); } filters.name = name; } return k8sExec(model, filters, FetchMethods.GET); } /** * @inheritdoc * @see {@link K8sInit.Delete} */ async function Delete(filter) { if (typeof filter === "string") { filters.name = filter; } else if (filter) { syncFilters(filter); } try { // Try to delete the resource await k8sExec(model, filters, 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, applyCfg = { force: false }) { syncFilters(resource); return k8sExec(model, filters, FetchMethods.APPLY, resource, applyCfg); } /** * @inheritdoc * @see {@link K8sInit.Create} */ async function Create(resource) { syncFilters(resource); return k8sExec(model, filters, FetchMethods.POST, resource); } /** * @inheritdoc * @see {@link K8sInit.Evict} */ async function Evict(filter) { 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(model, filters, FetchMethods.POST, 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) { // If there are no operations, throw an error if (payload.length < 1) { throw new Error("No operations specified"); } return k8sExec(model, filters, FetchMethods.PATCH, payload); } /** * @inheritdoc * @see {@link K8sInit.PatchStatus} */ async function PatchStatus(resource) { syncFilters(resource); return k8sExec(model, filters, FetchMethods.PATCH_STATUS, resource); } /** * @inheritdoc * @see {@link K8sInit.Watch} */ function Watch(callback, watchCfg) { return new Watcher(model, filters, callback, watchCfg); } /** * @inheritdoc * @see {@link K8sInit.Raw} */ async function Raw(url, method = FetchMethods.GET) { const thing = await k8sCfg(method); const { opts, serverUrl } = thing; const resp = await fetch(`${serverUrl}${url}`, opts); if (resp.ok) { return resp.data; } throw resp; } return { InNamespace, Apply, Create, Patch, PatchStatus, Raw, ...withFilters }; }