- Added a new `rollback` command to roll back deployments to their previous release. - Introduced `findRollbackCandidates` and `rollbackDeployment` functions to handle rollback logic. - Updated CLI command definitions to include the new `rollback` command with an alias. - Enhanced deployment conversion to use resolved images for buildable services. - Introduced a new `graph` module to visualize Kubernetes resources and their relationships. - Added tests for the new rollback functionality and graph rendering. - Updated package version to 1.3.0.
176 lines
4.7 KiB
TypeScript
176 lines
4.7 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import type { KubernetesObject } from "@kubernetes/client-node";
|
|
import {
|
|
buildNamespaceGraphs,
|
|
deriveNodeStatus,
|
|
renderNamespaceGraphs,
|
|
} from "../../lib/graph";
|
|
|
|
type TestObject = KubernetesObject & {
|
|
spec?: Record<string, unknown>;
|
|
status?: Record<string, unknown>;
|
|
};
|
|
|
|
const object = (
|
|
kind: string,
|
|
name: string,
|
|
extra: Partial<TestObject> = {},
|
|
): TestObject => {
|
|
const { metadata, ...rest } = extra;
|
|
return {
|
|
apiVersion: "v1",
|
|
kind,
|
|
...rest,
|
|
metadata: { name, namespace: "demo", ...metadata },
|
|
};
|
|
};
|
|
|
|
describe("ANSI resource graph", () => {
|
|
test("builds a deterministic owner tree and prefers owner edges", () => {
|
|
const deployment = object("Deployment", "web", {
|
|
status: { replicas: 1, readyReplicas: 1 },
|
|
});
|
|
const replicaSet = object("ReplicaSet", "web-123", {
|
|
metadata: {
|
|
ownerReferences: [
|
|
{ apiVersion: "apps/v1", kind: "Deployment", name: "web", uid: "1" },
|
|
],
|
|
},
|
|
status: { replicas: 1, readyReplicas: 1 },
|
|
});
|
|
const pod = object("Pod", "web-123-abc", {
|
|
metadata: {
|
|
labels: { app: "web" },
|
|
ownerReferences: [
|
|
{
|
|
apiVersion: "apps/v1",
|
|
kind: "ReplicaSet",
|
|
name: "web-123",
|
|
uid: "2",
|
|
},
|
|
],
|
|
},
|
|
status: {
|
|
phase: "Running",
|
|
conditions: [{ type: "Ready", status: "True" }],
|
|
},
|
|
});
|
|
const service = object("Service", "web", {
|
|
spec: { selector: { app: "web" } },
|
|
});
|
|
|
|
const [graph] = buildNamespaceGraphs([
|
|
pod,
|
|
service,
|
|
replicaSet,
|
|
deployment,
|
|
]);
|
|
expect(graph?.roots.map((node) => node.id)).toEqual([
|
|
"Deployment/web",
|
|
"Service/web",
|
|
]);
|
|
expect(graph?.roots[0]?.children[0]?.id).toBe("ReplicaSet/web-123");
|
|
expect(graph?.roots[0]?.children[0]?.children[0]?.id).toBe(
|
|
"Pod/web-123-abc",
|
|
);
|
|
expect(graph?.roots[1]?.status).toEqual({ label: "1 ep", level: "good" });
|
|
});
|
|
|
|
test("hides stopped Deployment subtrees unless includeIdle is set", () => {
|
|
const deployment = object("Deployment", "stopped", {
|
|
status: { replicas: 0, readyReplicas: 0 },
|
|
});
|
|
const replicaSet = object("ReplicaSet", "stopped-old", {
|
|
metadata: {
|
|
ownerReferences: [
|
|
{
|
|
apiVersion: "apps/v1",
|
|
kind: "Deployment",
|
|
name: "stopped",
|
|
uid: "1",
|
|
},
|
|
],
|
|
},
|
|
status: { replicas: 0, readyReplicas: 0 },
|
|
});
|
|
const pod = object("Pod", "stopped-old-pod", {
|
|
metadata: {
|
|
ownerReferences: [
|
|
{
|
|
apiVersion: "apps/v1",
|
|
kind: "ReplicaSet",
|
|
name: "stopped-old",
|
|
uid: "2",
|
|
},
|
|
],
|
|
},
|
|
status: { phase: "Failed" },
|
|
});
|
|
|
|
expect(
|
|
buildNamespaceGraphs([deployment, replicaSet, pod])[0]?.roots,
|
|
).toEqual([]);
|
|
const roots = buildNamespaceGraphs([deployment, replicaSet, pod], {
|
|
includeIdle: true,
|
|
})[0]?.roots;
|
|
expect(roots?.[0]?.id).toBe("Deployment/stopped");
|
|
expect(roots?.[0]?.children[0]?.children[0]?.id).toBe(
|
|
"Pod/stopped-old-pod",
|
|
);
|
|
});
|
|
|
|
test("derives good, warning, and bad statuses", () => {
|
|
expect(
|
|
deriveNodeStatus(
|
|
object("Pod", "ready", {
|
|
status: {
|
|
phase: "Running",
|
|
conditions: [{ type: "Ready", status: "True" }],
|
|
},
|
|
}),
|
|
),
|
|
).toEqual({ label: "ready", level: "good" });
|
|
expect(
|
|
deriveNodeStatus(
|
|
object("Deployment", "progressing", {
|
|
status: { replicas: 3, readyReplicas: 1 },
|
|
}),
|
|
),
|
|
).toEqual({ label: "1/3", level: "warn" });
|
|
expect(
|
|
deriveNodeStatus(
|
|
object("Pod", "failed", { status: { phase: "Failed" } }),
|
|
),
|
|
).toEqual({
|
|
label: "failed",
|
|
level: "bad",
|
|
});
|
|
});
|
|
|
|
test("renders tree connectors, kind colors, and ANSI status badges", () => {
|
|
const output = renderNamespaceGraphs([
|
|
{
|
|
namespace: "demo",
|
|
roots: [
|
|
{
|
|
id: "Deployment/web",
|
|
status: { label: "1/1", level: "good" },
|
|
children: [
|
|
{
|
|
id: "Pod/web-abc",
|
|
status: { label: "pending", level: "warn" },
|
|
children: [],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
expect(output).toContain("demo\n└─ \u001b[34mDeployment/web\u001b[0m");
|
|
expect(output).toContain("\u001b[42m\u001b[30m 1/1 \u001b[0m");
|
|
expect(output).toContain(" └─ \u001b[32mPod/web-abc\u001b[0m");
|
|
expect(output).toContain("\u001b[43m\u001b[30m pending \u001b[0m");
|
|
});
|
|
});
|