Files
pikasTech-unidesk/scripts/src/platform-infra-observability/summary.ts
T
Lyon edfddd2445 fix: YAML-first 治理 CI/CD target (#919)
* docs: specify cicd yaml target governance

* fix: resolve cicd targets from yaml

---------

Co-authored-by: Codex <codex@noreply.local>
2026-06-26 01:14:38 +08:00

233 lines
10 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. summary module for scripts/src/platform-infra-observability.ts.
// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets.
// Moved mechanically from scripts/src/platform-infra-observability.ts:3031-4400 for #903.
// SPEC: PJ2026-01060501 OTel追踪 draft-2026-06-19-p0.
// Responsibility: YAML-first platform-infra OpenTelemetry tracing control commands.
import { Buffer } from "node:buffer";
import { readFileSync } from "node:fs";
import type { UniDeskConfig } from "../config";
import { rootPath } from "../config";
import type { RenderedCliResult } from "../output";
import {
compactCapture,
createYamlFieldReader,
numberField,
parseJsonOutput,
redactSensitiveUnknown,
shQuote,
capture,
} from "../platform-infra-ops-library";
import type { ImageSpec, ObservabilityConfig, ObservabilityTarget } from "./types";
import { status } from "./actions";
import { asRecord, configLabel } from "./types";
export function configSummary(observability: ObservabilityConfig, target: ObservabilityTarget): Record<string, unknown> {
return {
configPath: configLabel,
spec: observability.metadata.spec,
target: targetSummary(target),
images: {
collector: imageReference(observability.images.collector),
traceBackend: imageReference(observability.images.tempo),
},
collector: observability.collector,
traceBackend: observability.traceBackend,
sampling: observability.sampling,
serviceConnections: observability.instrumentation.serviceConnections.map((item) => ({
serviceName: item.serviceName,
owningRepo: item.owningRepo,
resolved: {
targetNode: item.targetNode,
lane: item.lane,
namespace: item.namespace,
},
configRefs: item.configRefGraph,
requiredSpans: item.requiredSpans,
})),
};
}
export function targetSummary(target: ObservabilityTarget): Record<string, unknown> {
return {
id: target.id,
route: target.route,
namespace: target.namespace,
role: target.role,
createNamespace: target.createNamespace,
};
}
export function policyChecks(yaml: string, target: ObservabilityTarget): Array<Record<string, unknown>> {
return [
{ name: "yaml-source-of-truth", ok: true, detail: "All concrete images, routes, namespace, ports, retention and sampling values are read from config/platform-infra/observability.yaml." },
{ name: "clusterip-only", ok: !/^\s*type:\s*(NodePort|LoadBalancer)\s*$/mu.test(yaml), detail: "Collector and trace backend stay ClusterIP-only." },
{ name: "no-ingress", ok: !/^\s*kind:\s*Ingress\s*$/mu.test(yaml), detail: "No public ingress is rendered for the first tracing backend." },
{ name: "no-host-network", ok: !/^\s*hostNetwork:\s*true\s*$/mu.test(yaml), detail: "Pods must not use host network." },
{ name: "allow-all-network-policy", ok: yaml.includes("kind: NetworkPolicy") && yaml.includes("name: allow-all") && yaml.includes(`namespace: ${target.namespace}`), detail: `NetworkPolicy/allow-all is rendered in ${target.namespace}.` },
];
}
export function statusSummary(payload: Record<string, unknown>): Record<string, unknown> {
const sections = asRecord(payload.sections, "status.sections");
const deployments = objectList(sectionJson(sections, "deployments"));
const services = objectList(sectionJson(sections, "services"));
const pods = objectList(sectionJson(sections, "pods"));
const events = objectList(sectionJson(sections, "events"));
const podEvents = latestPodEvents(events);
const probes = Array.isArray(payload.probes) ? payload.probes as Array<Record<string, unknown>> : [];
const readyDeployments = deployments.map((item) => deploymentSummary(item));
return {
ready: payload.ok === true,
namespace: payload.namespace,
deployments: readyDeployments,
services: services.map((item) => metadataName(item)),
pods: pods.map((item) => podSummary(item, podEvents)),
probes: probes.map((item) => ({
name: item.name,
ok: item.ok === true,
service: item.service,
path: item.path,
stderrTail: item.ok === true ? "" : item.stderrTail,
})),
};
}
export function compactStatus(payload: Record<string, unknown> | null, full: boolean): Record<string, unknown> | null {
if (payload === null) return null;
if (full) return redactSensitiveUnknown(payload) as Record<string, unknown>;
return statusSummary(payload);
}
export function sectionJson(sections: Record<string, unknown>, name: string): unknown {
const section = asRecord(sections[name], `sections.${name}`);
return section.json;
}
export function objectList(value: unknown): Record<string, unknown>[] {
if (typeof value !== "object" || value === null) return [];
const items = (value as Record<string, unknown>).items;
if (!Array.isArray(items)) return [];
return items.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item));
}
export function deploymentSummary(item: Record<string, unknown>): Record<string, unknown> {
const spec = item.spec as Record<string, unknown> | undefined;
const status = item.status as Record<string, unknown> | undefined;
const replicas = typeof spec?.replicas === "number" ? spec.replicas : null;
const available = typeof status?.availableReplicas === "number" ? status.availableReplicas : 0;
return {
name: metadataName(item),
replicas,
availableReplicas: available,
ready: replicas !== null && available >= replicas,
};
}
export function podSummary(item: Record<string, unknown>, podEvents: Map<string, Record<string, unknown>>): Record<string, unknown> {
const status = item.status as Record<string, unknown> | undefined;
const waiting = firstWaitingContainer(status);
const scheduled = conditionSummary(status, "PodScheduled");
const ready = conditionSummary(status, "Ready");
const name = metadataName(item);
const showEvent = status?.phase !== "Running" || waiting !== null || scheduled !== null || ready !== null;
const latestEvent = showEvent && name !== null ? podEvents.get(name) ?? null : null;
return {
name,
phase: status?.phase ?? null,
reason: waiting?.reason ?? scheduled?.reason ?? ready?.reason ?? null,
message: waiting?.message ?? scheduled?.message ?? ready?.message ?? null,
latestEvent: latestEvent === null ? null : {
reason: stringValue(latestEvent.reason),
message: stringValue(latestEvent.message),
type: stringValue(latestEvent.type),
count: typeof latestEvent.count === "number" ? latestEvent.count : null,
timestamp: eventTimestamp(latestEvent),
},
};
}
export function latestPodEvents(events: Record<string, unknown>[]): Map<string, Record<string, unknown>> {
const byPod = new Map<string, Record<string, unknown>>();
for (const item of events) {
const involved = item.involvedObject;
if (typeof involved !== "object" || involved === null || Array.isArray(involved)) continue;
const podName = (involved as Record<string, unknown>).name;
const kind = (involved as Record<string, unknown>).kind;
if (kind !== "Pod" || typeof podName !== "string") continue;
const current = byPod.get(podName);
if (current === undefined || eventTimestamp(item) > eventTimestamp(current)) byPod.set(podName, item);
}
return byPod;
}
export function eventTimestamp(item: Record<string, unknown>): string | null {
const candidate = stringValue(item.eventTime)
?? stringValue(item.lastTimestamp)
?? stringValue(item.deprecatedLastTimestamp)
?? (typeof item.metadata === "object" && item.metadata !== null && !Array.isArray(item.metadata)
? stringValue((item.metadata as Record<string, unknown>).creationTimestamp)
: null);
return candidate;
}
export function stringValue(value: unknown): string | null {
return typeof value === "string" && value.length > 0 ? value : null;
}
export function firstWaitingContainer(status: Record<string, unknown> | undefined): { reason: string | null; message: string | null } | null {
const statuses = Array.isArray(status?.containerStatuses) ? status.containerStatuses : [];
for (const item of statuses) {
if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
const state = (item as Record<string, unknown>).state;
if (typeof state !== "object" || state === null || Array.isArray(state)) continue;
const waiting = (state as Record<string, unknown>).waiting;
if (typeof waiting !== "object" || waiting === null || Array.isArray(waiting)) continue;
const record = waiting as Record<string, unknown>;
return {
reason: typeof record.reason === "string" ? record.reason : null,
message: typeof record.message === "string" ? record.message : null,
};
}
return null;
}
export function conditionSummary(status: Record<string, unknown> | undefined, type: string): { reason: string | null; message: string | null } | null {
const conditions = Array.isArray(status?.conditions) ? status.conditions : [];
for (const item of conditions) {
if (typeof item !== "object" || item === null || Array.isArray(item)) continue;
const record = item as Record<string, unknown>;
if (record.type !== type || record.status === "True") continue;
return {
reason: typeof record.reason === "string" ? record.reason : null,
message: typeof record.message === "string" ? record.message : null,
};
}
return null;
}
export function metadataName(item: Record<string, unknown>): string | null {
const metadata = item.metadata as Record<string, unknown> | undefined;
return typeof metadata?.name === "string" ? metadata.name : null;
}
export function manifestObjectSummary(yaml: string): Array<Record<string, string>> {
const docs = yaml.split(/^---$/mu);
return docs.map((doc) => {
const kind = doc.match(/^\s*kind:\s*(.+)$/mu)?.[1]?.trim() ?? "unknown";
const name = doc.match(/^\s*name:\s*(.+)$/mu)?.[1]?.trim() ?? "unknown";
return { kind, name };
});
}
export function imageReference(image: ImageSpec): string {
return `${image.repository}:${image.tag}`;
}
export function indent(value: string, spaces: number): string {
const prefix = " ".repeat(spaces);
return value.split("\n").map((line) => `${prefix}${line}`).join("\n");
}