import type { KubernetesObject } from "@kubernetes/client-node"; type GraphObject = KubernetesObject & { spec?: Record; status?: Record; }; type Edge = { from: string; to: string; type: "owner" | "hpa" | "service" | "ingress" | "volume"; }; export type NodeStatus = { label: string; level: "good" | "warn" | "bad"; }; export type GraphNode = { id: string; status: NodeStatus; children: GraphNode[]; }; export type NamespaceGraph = { namespace: string; roots: GraphNode[]; }; const resources = [ ["apps/v1", "Deployment"], ["apps/v1", "StatefulSet"], ["apps/v1", "DaemonSet"], ["apps/v1", "ReplicaSet"], ["v1", "Pod"], ["v1", "Service"], ["networking.k8s.io/v1", "Ingress"], ["autoscaling/v2", "HorizontalPodAutoscaler"], ["v1", "PersistentVolumeClaim"], ["v1", "ConfigMap"], ["v1", "Secret"], ["batch/v1", "Job"], ["batch/v1", "CronJob"], ["policy/v1", "PodDisruptionBudget"], ] as const; const ansi = { reset: "\u001b[0m", fgBlack: "\u001b[30m", fgBlue: "\u001b[34m", fgPurple: "\u001b[35m", fgYellow: "\u001b[33m", fgGreen: "\u001b[32m", bgGreen: "\u001b[42m", bgYellow: "\u001b[43m", bgRed: "\u001b[41m", }; function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } function getKind(object: GraphObject): string { return object.kind ?? "Unknown"; } function getName(object: GraphObject): string { return object.metadata?.name ?? "unknown"; } function getId(object: GraphObject): string { return `${getKind(object)}/${getName(object)}`; } function getNamespace(object: GraphObject): string { return object.metadata?.namespace ?? "default"; } function number(value: unknown): number | undefined { return typeof value === "number" ? value : undefined; } function hasCondition( object: GraphObject, type: string, status: string, ): boolean { const conditions = Array.isArray(object.status?.conditions) ? object.status.conditions : []; return conditions.some( (condition) => isRecord(condition) && condition.type === type && condition.status === status, ); } function stringRecord(value: unknown): Record | undefined { if (!isRecord(value)) return undefined; const entries = Object.entries(value); if (entries.some(([, item]) => typeof item !== "string")) return undefined; return Object.fromEntries(entries) as Record; } function matchesSelector( selector: Record | undefined, labels: Record | undefined, ): boolean { const entries = Object.entries(selector ?? {}); return ( entries.length > 0 && entries.every(([key, value]) => labels?.[key] === value) ); } function priority(type: Edge["type"]): number { return { owner: 5, hpa: 4, ingress: 3, service: 2, volume: 1 }[type]; } function pushEdge(edges: Edge[], seen: Set, edge: Edge): void { const key = `${edge.from}\0${edge.to}\0${edge.type}`; if (seen.has(key)) return; seen.add(key); edges.push(edge); } function collectEdges(items: GraphObject[]): Edge[] { const edges: Edge[] = []; const seen = new Set(); const pods = items.filter((item) => getKind(item) === "Pod"); for (const child of items) { for (const owner of child.metadata?.ownerReferences ?? []) { if (owner.kind && owner.name) { pushEdge(edges, seen, { from: `${owner.kind}/${owner.name}`, to: getId(child), type: "owner", }); } } } for (const hpa of items.filter( (item) => getKind(item) === "HorizontalPodAutoscaler", )) { const target = isRecord(hpa.spec?.scaleTargetRef) ? hpa.spec.scaleTargetRef : undefined; if (typeof target?.kind === "string" && typeof target.name === "string") { pushEdge(edges, seen, { from: getId(hpa), to: `${target.kind}/${target.name}`, type: "hpa", }); } } for (const service of items.filter((item) => getKind(item) === "Service")) { const selector = stringRecord(service.spec?.selector); for (const pod of pods) { if (matchesSelector(selector, pod.metadata?.labels)) { pushEdge(edges, seen, { from: getId(service), to: getId(pod), type: "service", }); } } } for (const ingress of items.filter((item) => getKind(item) === "Ingress")) { const serviceNames = new Set(); const defaultBackend = isRecord(ingress.spec?.defaultBackend) ? ingress.spec.defaultBackend : undefined; const defaultService = isRecord(defaultBackend?.service) ? defaultBackend.service : undefined; if (typeof defaultService?.name === "string") serviceNames.add(defaultService.name); const rules = Array.isArray(ingress.spec?.rules) ? ingress.spec.rules : []; for (const rule of rules) { if (!isRecord(rule) || !isRecord(rule.http)) continue; const paths = Array.isArray(rule.http.paths) ? rule.http.paths : []; for (const path of paths) { if (!isRecord(path) || !isRecord(path.backend)) continue; const service = isRecord(path.backend.service) ? path.backend.service : undefined; if (typeof service?.name === "string") serviceNames.add(service.name); } } for (const name of serviceNames) { pushEdge(edges, seen, { from: getId(ingress), to: `Service/${name}`, type: "ingress", }); } } for (const pod of pods) { const volumes = Array.isArray(pod.spec?.volumes) ? pod.spec.volumes : []; for (const volume of volumes) { if (!isRecord(volume)) continue; const pvc = isRecord(volume.persistentVolumeClaim) ? volume.persistentVolumeClaim : undefined; const configMap = isRecord(volume.configMap) ? volume.configMap : undefined; const secret = isRecord(volume.secret) ? volume.secret : undefined; const targets = [ ["PersistentVolumeClaim", pvc?.claimName], ["ConfigMap", configMap?.name], ["Secret", secret?.secretName], ] as const; for (const [kind, name] of targets) { if (typeof name === "string") { pushEdge(edges, seen, { from: getId(pod), to: `${kind}/${name}`, type: "volume", }); } } } } return edges; } export function deriveNodeStatus( object: GraphObject | undefined, serviceTargetCount = 0, ): NodeStatus { if (!object) return { label: "warn", level: "warn" }; const status = object.status; switch (getKind(object)) { case "Pod": { const phase = typeof status?.phase === "string" ? status.phase : "Unknown"; if (phase === "Running" && hasCondition(object, "Ready", "True")) return { label: "ready", level: "good" }; if (phase === "Pending") return { label: "pending", level: "warn" }; if (phase === "Succeeded") return { label: "done", level: "good" }; return { label: phase.toLowerCase(), level: "bad" }; } case "Deployment": case "ReplicaSet": case "StatefulSet": { const ready = number(status?.readyReplicas) ?? 0; const desired = number(status?.replicas) ?? 0; if (desired === 0) return { label: "idle", level: "warn" }; if (ready === desired) return { label: `${ready}/${desired}`, level: "good" }; if (ready > 0) return { label: `${ready}/${desired}`, level: "warn" }; return { label: `${ready}/${desired}`, level: "bad" }; } case "DaemonSet": { const ready = number(status?.numberReady) ?? 0; const desired = number(status?.desiredNumberScheduled) ?? 0; if (desired === 0) return { label: "idle", level: "warn" }; if (ready === desired) return { label: `${ready}/${desired}`, level: "good" }; if (ready > 0) return { label: `${ready}/${desired}`, level: "warn" }; return { label: `${ready}/${desired}`, level: "bad" }; } case "Job": if (hasCondition(object, "Complete", "True")) return { label: "done", level: "good" }; if (hasCondition(object, "Failed", "True")) return { label: "failed", level: "bad" }; return { label: "running", level: "warn" }; case "CronJob": return object.spec?.suspend === true ? { label: "paused", level: "warn" } : { label: "ok", level: "good" }; case "PersistentVolumeClaim": { const phase = typeof status?.phase === "string" ? status.phase : "Unknown"; if (phase === "Bound") return { label: "bound", level: "good" }; if (phase === "Pending") return { label: "pending", level: "warn" }; return { label: phase.toLowerCase(), level: "bad" }; } case "Service": return serviceTargetCount > 0 ? { label: `${serviceTargetCount} ep`, level: "good" } : { label: "no ep", level: "warn" }; case "Ingress": return { label: "route", level: "good" }; case "HorizontalPodAutoscaler": { const current = number(status?.currentReplicas); return current === undefined ? { label: "warn", level: "warn" } : { label: `${current} cur`, level: "good" }; } case "PodDisruptionBudget": { const healthy = number(status?.currentHealthy) ?? 0; const desired = number(status?.desiredHealthy) ?? 0; if (healthy >= desired) return { label: `${healthy}/${desired}`, level: "good" }; if (healthy > 0) return { label: `${healthy}/${desired}`, level: "warn" }; return { label: `${healthy}/${desired}`, level: "bad" }; } case "ConfigMap": case "Secret": return { label: "ok", level: "good" }; default: return { label: "ok", level: "warn" }; } } function isIdle(object: GraphObject | undefined): boolean { return ( (getKind(object ?? {}) === "Deployment" || getKind(object ?? {}) === "ReplicaSet") && (number(object?.status?.replicas) ?? 0) === 0 && (number(object?.status?.readyReplicas) ?? 0) === 0 ); } export function buildNamespaceGraphs( items: GraphObject[], options: { includeIdle?: boolean } = {}, ): NamespaceGraph[] { const byNamespace = new Map(); for (const item of items) { const bucket = byNamespace.get(getNamespace(item)) ?? []; bucket.push(item); byNamespace.set(getNamespace(item), bucket); } return [...byNamespace.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([namespace, namespaceItems]) => { const objectById = new Map( namespaceItems.map((item) => [getId(item), item]), ); const edges = collectEdges(namespaceItems).sort((left, right) => `${left.from}->${left.to}`.localeCompare(`${right.from}->${right.to}`), ); const allNodes = new Set(namespaceItems.map(getId)); const bestIncoming = new Map(); const serviceTargets = new Map>(); for (const edge of edges) { if (!objectById.has(edge.from) || !objectById.has(edge.to)) continue; if (edge.type === "service") { const targets = serviceTargets.get(edge.from) ?? new Set(); targets.add(edge.to); serviceTargets.set(edge.from, targets); } const current = bestIncoming.get(edge.to); if (!current || priority(edge.type) > priority(current.type)) bestIncoming.set(edge.to, edge); } const children = new Map(); for (const edge of bestIncoming.values()) { const bucket = children.get(edge.from) ?? []; bucket.push(edge.to); children.set(edge.from, bucket); } const hidden = new Set(); const hideSubtree = (id: string): void => { if (hidden.has(id)) return; hidden.add(id); for (const child of children.get(id) ?? []) hideSubtree(child); }; if (!options.includeIdle) { for (const item of namespaceItems) { if (getKind(item) === "Deployment" && isIdle(item)) hideSubtree(getId(item)); } } for (const [parent, childIds] of children) { if (getKind(objectById.get(parent) ?? {}) !== "Deployment") continue; const replicaSets = childIds.filter( (id) => getKind(objectById.get(id) ?? {}) === "ReplicaSet", ); if (!replicaSets.some((id) => !isIdle(objectById.get(id)))) continue; for (const id of replicaSets.filter((id) => isIdle(objectById.get(id)))) hideSubtree(id); children.set( parent, childIds.filter((id) => !hidden.has(id)), ); } const buildNode = (id: string): GraphNode => ({ id, status: deriveNodeStatus( objectById.get(id), serviceTargets.get(id)?.size, ), children: [...new Set(children.get(id) ?? [])] .filter((child) => !hidden.has(child)) .sort((left, right) => left.localeCompare(right)) .map(buildNode), }); const printed = new Set(); const roots: GraphNode[] = []; const addRoot = (id: string): void => { if (hidden.has(id) || printed.has(id)) return; const root = buildNode(id); const mark = (node: GraphNode): void => { printed.add(node.id); node.children.forEach(mark); }; mark(root); roots.push(root); }; [...allNodes] .filter((id) => !bestIncoming.has(id)) .sort((left, right) => left.localeCompare(right)) .forEach(addRoot); [...allNodes] .sort((left, right) => left.localeCompare(right)) .forEach(addRoot); return { namespace, roots }; }); } export async function fetchNamespaceObjects( namespace: string, ): Promise { const { objectApi } = await import("./k8s"); const lists = await Promise.all( resources.map(async ([apiVersion, kind]) => { const list = await objectApi.list(apiVersion, kind, namespace); return list.items.map((item) => ({ ...item, apiVersion: item.apiVersion ?? apiVersion, kind: item.kind ?? kind, metadata: { ...item.metadata, namespace: item.metadata?.namespace ?? namespace, }, })) as GraphObject[]; }), ); return lists.flat(); } function badge(status: NodeStatus): string { const background = status.level === "good" ? ansi.bgGreen : status.level === "bad" ? ansi.bgRed : ansi.bgYellow; return `${background}${ansi.fgBlack} ${status.label} ${ansi.reset}`; } function colorize(id: string): string { const kind = id.split("/", 1)[0]; const color = kind === "Deployment" ? ansi.fgBlue : kind === "Service" ? ansi.fgPurple : kind === "Ingress" ? ansi.fgYellow : kind === "Pod" ? ansi.fgGreen : undefined; return color ? `${color}${id}${ansi.reset}` : id; } export function renderNamespaceGraphs(graphs: NamespaceGraph[]): string { const lines: string[] = []; const renderNode = (node: GraphNode, prefix: string, last: boolean): void => { lines.push( `${prefix}${last ? "└─ " : "├─ "}${colorize(node.id)} ${badge(node.status)}`, ); const childPrefix = prefix + (last ? " " : "│ "); node.children.forEach((child, index) => { renderNode(child, childPrefix, index === node.children.length - 1); }); }; for (const graph of graphs) { lines.push(graph.namespace); graph.roots.forEach((root, index) => { renderNode(root, "", index === graph.roots.length - 1); }); lines.push(""); } return lines.join("\n"); }