edfddd2445
* docs: specify cicd yaml target governance * fix: resolve cicd targets from yaml --------- Co-authored-by: Codex <codex@noreply.local>
233 lines
10 KiB
TypeScript
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");
|
|
}
|