Skip to content

Commit

Permalink
feat: log support in KFC (#238)
Browse files Browse the repository at this point in the history
Fixes #214 

A need was brought up for the KFC to have a logging mechanism. This will
make e2e and journey testing clusters for UDS and Software Factory
easier.

---------

Signed-off-by: Case Wylie <cmwylie19@defenseunicorns.com>
  • Loading branch information
cmwylie19 committed May 6, 2024
1 parent 625eaf0 commit decf48a
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 6 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ async function demo() {
runningPods.items.forEach(pod => {
console.log(`${pod.metadata?.namespace}/${pod.metadata?.name} is running`);
});

// Get logs from a Deployment named "nginx" in the namespace
const logs = await K8s(kind.Deployment).InNamespace(namespace).Logs("nginx");
console.log(logs);
}

// Create a few resources to work with: Namespace, ConfigMap, and Pod
Expand Down
81 changes: 79 additions & 2 deletions src/fluent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import { GenericClass } from "../types";
import { ApplyCfg, FetchMethods, Filters, K8sInit, Paths, WatchAction } from "./types";
import { k8sCfg, k8sExec } from "./utils";
import { WatchCfg, Watcher } from "./watch";

import { hasLogs } from "../helpers";
import { Pod, Service, ReplicaSet } from "../upstream";
/**
* Kubernetes fluent API inspired by Kubectl. Pass in a model, then call filters and actions on it.
*
Expand All @@ -24,7 +25,7 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
model: T,
filters: Filters = {},
): K8sInit<T, K> {
const withFilters = { WithField, WithLabel, Get, Delete, Watch };
const withFilters = { WithField, WithLabel, Get, Delete, Watch, Logs };
const matchedKind = filters.kindOverride || modelToGroupVersionKind(model.name);

/**
Expand Down Expand Up @@ -87,7 +88,83 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
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, "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 (e) {
throw new Error(e);
}

const podModel = { ...model, name: "V1Pod" };
const logPromises = podList.map(po =>
k8sExec<T, string>(podModel, { ...filters, name: po.metadata!.name! }, "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>;
/**
Expand Down
3 changes: 2 additions & 1 deletion src/fluent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export type FetchMethods =
| "DELETE"
| "PATCH"
| "WATCH"
| "PATCH_STATUS";
| "PATCH_STATUS"
| "LOG";

export interface Filters {
kindOverride?: GroupVersionKind;
Expand Down
17 changes: 15 additions & 2 deletions src/fluent/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,21 @@ export async function k8sExec<T extends GenericClass, K>(
payload?: K | unknown,
applyCfg: ApplyCfg = { force: false },
) {
const { opts, serverUrl } = await k8sCfg(method);
const url = pathBuilder(serverUrl, model, filters, method === "POST");
const reconstruct = async (method: FetchMethods) => {
const configMethod = method === "LOG" ? "GET" : method;
const { opts, serverUrl } = await k8sCfg(configMethod);
const isPost = method === "POST";
const baseUrl = pathBuilder(serverUrl, model, filters, isPost);
if (method === "LOG") {
baseUrl.pathname = `${baseUrl.pathname}/log`;
}
return {
url: baseUrl,
opts,
};
};

const { opts, url } = await reconstruct(method);

switch (opts.method) {
// PATCH_STATUS is a special case that uses the PATCH method on status subresources
Expand Down
19 changes: 18 additions & 1 deletion src/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { describe, expect, it, test } from "@jest/globals";

import { fromEnv, waitForCluster } from "./helpers";
import { fromEnv, hasLogs, waitForCluster } from "./helpers";

describe("helpers", () => {
test("fromEnv for NodeJS", () => {
Expand All @@ -23,3 +23,20 @@ describe("Cluster Wait Function", () => {
expect(cluster).toEqual({ server: "http://jest-test:8080" });
});
});

describe("hasLogs function", () => {
it("should return true for known kinds", () => {
expect(hasLogs("Pod")).toBe(true);
expect(hasLogs("DaemonSet")).toBe(true);
expect(hasLogs("ReplicaSet")).toBe(true);
expect(hasLogs("Service")).toBe(true);
expect(hasLogs("StatefulSet")).toBe(true);
expect(hasLogs("Deployment")).toBe(true);
});

it("should return false for unknown kinds", () => {
expect(hasLogs("Unknown")).toBe(false);
expect(hasLogs("")).toBe(false);
expect(hasLogs("RandomKind")).toBe(false);
});
});
31 changes: 31 additions & 0 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,34 @@ export async function waitForCluster(seconds = 30): Promise<Cluster> {

return cluster;
}

/**
* Determines if object has logs.
*
* @param kind The kind of Kubernetes object.
* @returns boolean.
*/
export function hasLogs(kind: string): boolean {
let hasSelector: boolean = false;
switch (kind) {
case "Pod":
hasSelector = true;
break;
case "DaemonSet":
hasSelector = true;
break;
case "ReplicaSet":
hasSelector = true;
break;
case "Service":
hasSelector = true;
break;
case "StatefulSet":
hasSelector = true;
break;
case "Deployment":
hasSelector = true;
break;
}
return hasSelector;
}

0 comments on commit decf48a

Please sign in to comment.