edfddd2445
* docs: specify cicd yaml target governance * fix: resolve cicd targets from yaml --------- Co-authored-by: Codex <codex@noreply.local>
169 lines
8.8 KiB
TypeScript
169 lines
8.8 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. config 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:370-514 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, OtlpPorts, ServiceConnection } from "./types";
|
|
import { assertKnownEnabledTarget, parseStatusEndpoint } from "./actions";
|
|
import { apiPathField, arrayOfRecords, asRecord, booleanField, configFile, configLabel, enumField, integerField, kubernetesNameField, numberArrayField, objectField, portField, stringArrayField, stringField } from "./types";
|
|
import { configRefGraph, resolveConfigRefString } from "../ops/config-refs";
|
|
|
|
export function readObservabilityConfig(): ObservabilityConfig {
|
|
const parsed = Bun.YAML.parse(readFileSync(configFile, "utf8")) as unknown;
|
|
const root = asRecord(parsed, configLabel);
|
|
const version = integerField(root, "version", "");
|
|
const kind = stringField(root, "kind", "");
|
|
if (kind !== "platform-infra-observability") throw new Error(`${configLabel}.kind must be platform-infra-observability`);
|
|
const metadata = objectField(root, "metadata", "");
|
|
const defaults = objectField(root, "defaults", "");
|
|
const images = objectField(root, "images", "");
|
|
const collector = objectField(root, "collector", "");
|
|
const collectorOtlp = objectField(collector, "otlp", "collector");
|
|
const traceBackend = objectField(root, "traceBackend", "");
|
|
const traceBackendOtlp = objectField(traceBackend, "otlp", "traceBackend");
|
|
const traceBackendStorage = objectField(traceBackend, "storage", "traceBackend");
|
|
const sampling = objectField(root, "sampling", "");
|
|
const instrumentation = objectField(root, "instrumentation", "");
|
|
const resourceAttributes = objectField(root, "resourceAttributes", "");
|
|
const probes = objectField(root, "probes", "");
|
|
const config: ObservabilityConfig = {
|
|
version,
|
|
kind,
|
|
metadata: {
|
|
id: stringField(metadata, "id", "metadata"),
|
|
owner: stringField(metadata, "owner", "metadata"),
|
|
spec: stringField(metadata, "spec", "metadata"),
|
|
relatedIssues: numberArrayField(metadata, "relatedIssues", "metadata"),
|
|
},
|
|
defaults: { targetId: stringField(defaults, "targetId", "defaults") },
|
|
images: {
|
|
collector: imageSpec(objectField(images, "collector", "images"), "images.collector"),
|
|
tempo: imageSpec(objectField(images, "tempo", "images"), "images.tempo"),
|
|
},
|
|
targets: arrayOfRecords(root.targets, "targets").map(parseTarget),
|
|
collector: {
|
|
deploymentName: kubernetesNameField(collector, "deploymentName", "collector"),
|
|
serviceName: kubernetesNameField(collector, "serviceName", "collector"),
|
|
configMapName: kubernetesNameField(collector, "configMapName", "collector"),
|
|
replicas: integerField(collector, "replicas", "collector"),
|
|
healthPort: portField(collector, "healthPort", "collector"),
|
|
otlp: parseOtlpPorts(collectorOtlp, "collector.otlp"),
|
|
},
|
|
traceBackend: {
|
|
type: enumField(traceBackend, "type", "traceBackend", ["tempo"] as const),
|
|
deploymentName: kubernetesNameField(traceBackend, "deploymentName", "traceBackend"),
|
|
serviceName: kubernetesNameField(traceBackend, "serviceName", "traceBackend"),
|
|
configMapName: kubernetesNameField(traceBackend, "configMapName", "traceBackend"),
|
|
replicas: integerField(traceBackend, "replicas", "traceBackend"),
|
|
httpPort: portField(traceBackend, "httpPort", "traceBackend"),
|
|
otlp: parseOtlpPorts(traceBackendOtlp, "traceBackend.otlp"),
|
|
storage: {
|
|
mode: enumField(traceBackendStorage, "mode", "traceBackend.storage", ["emptyDir"] as const),
|
|
retention: stringField(traceBackendStorage, "retention", "traceBackend.storage"),
|
|
},
|
|
},
|
|
sampling: {
|
|
mode: enumField(sampling, "mode", "sampling", ["parentbased_traceidratio"] as const),
|
|
ratio: numberField(sampling, "ratio", "sampling"),
|
|
},
|
|
instrumentation: {
|
|
contextPropagation: stringArrayField(instrumentation, "contextPropagation", "instrumentation"),
|
|
serviceConnections: arrayOfRecords(instrumentation.serviceConnections, "instrumentation.serviceConnections").map(parseServiceConnection),
|
|
},
|
|
resourceAttributes: {
|
|
required: stringArrayField(resourceAttributes, "required", "resourceAttributes"),
|
|
businessCorrelationAttributes: stringArrayField(resourceAttributes, "businessCorrelationAttributes", "resourceAttributes"),
|
|
},
|
|
probes: {
|
|
readinessPath: apiPathField(probes, "readinessPath", "probes"),
|
|
traceQueryPathTemplate: stringField(probes, "traceQueryPathTemplate", "probes"),
|
|
statusEndpoints: arrayOfRecords(probes.statusEndpoints, "probes.statusEndpoints").map(parseStatusEndpoint),
|
|
},
|
|
};
|
|
if (config.targets.length === 0) throw new Error(`${configLabel}.targets must not be empty`);
|
|
assertKnownEnabledTarget(config.targets, config.defaults.targetId, "defaults.targetId");
|
|
if (config.collector.replicas < 0 || config.traceBackend.replicas < 0) throw new Error(`${configLabel} replicas must be >= 0`);
|
|
if (config.sampling.ratio < 0 || config.sampling.ratio > 1) throw new Error(`${configLabel}.sampling.ratio must be between 0 and 1`);
|
|
if (!config.probes.traceQueryPathTemplate.includes("{{traceId}}")) throw new Error(`${configLabel}.probes.traceQueryPathTemplate must include {{traceId}}`);
|
|
return config;
|
|
}
|
|
|
|
export function imageSpec(record: Record<string, unknown>, path: string): ImageSpec {
|
|
const image = {
|
|
repository: stringField(record, "repository", path),
|
|
tag: stringField(record, "tag", path),
|
|
pullPolicy: enumField(record, "pullPolicy", path, ["Always", "IfNotPresent", "Never"] as const),
|
|
};
|
|
if (!/^[A-Za-z0-9._/:@-]+$/u.test(`${image.repository}:${image.tag}`)) throw new Error(`${configLabel}.${path} must render a valid image reference`);
|
|
return image;
|
|
}
|
|
|
|
export function parseTarget(record: Record<string, unknown>, index: number): ObservabilityTarget {
|
|
const path = `targets[${index}]`;
|
|
return {
|
|
id: stringField(record, "id", path),
|
|
route: stringField(record, "route", path),
|
|
namespace: kubernetesNameField(record, "namespace", path),
|
|
role: enumField(record, "role", path, ["active", "standby"] as const),
|
|
enabled: booleanField(record, "enabled", path),
|
|
createNamespace: booleanField(record, "createNamespace", path),
|
|
};
|
|
}
|
|
|
|
export function parseOtlpPorts(record: Record<string, unknown>, path: string): OtlpPorts {
|
|
return {
|
|
grpcPort: portField(record, "grpcPort", path),
|
|
httpPort: portField(record, "httpPort", path),
|
|
};
|
|
}
|
|
|
|
export function parseServiceConnection(record: Record<string, unknown>, index: number): ServiceConnection {
|
|
const path = `instrumentation.serviceConnections[${index}]`;
|
|
const refs = record.configRefs === undefined ? null : objectField(record, "configRefs", path);
|
|
const configRefs = refs === null
|
|
? {}
|
|
: {
|
|
targetNode: stringField(refs, "targetNode", `${path}.configRefs`),
|
|
lane: stringField(refs, "lane", `${path}.configRefs`),
|
|
namespace: stringField(refs, "namespace", `${path}.configRefs`),
|
|
};
|
|
return {
|
|
serviceName: stringField(record, "serviceName", path),
|
|
owningRepo: stringField(record, "owningRepo", path),
|
|
targetNode: refs === null ? stringField(record, "targetNode", path) : resolveConfigRefString(configRefs.targetNode, `${path}.configRefs.targetNode`),
|
|
lane: refs === null ? stringField(record, "lane", path) : resolveConfigRefString(configRefs.lane, `${path}.configRefs.lane`),
|
|
namespace: refs === null ? kubernetesNameField(record, "namespace", path) : kubernetesNameValue(resolveConfigRefString(configRefs.namespace, `${path}.configRefs.namespace`), `${path}.configRefs.namespace`),
|
|
configRefs,
|
|
configRefGraph: refs === null
|
|
? []
|
|
: configRefGraph([
|
|
{ id: `${path}.targetNode`, ref: configRefs.targetNode },
|
|
{ id: `${path}.lane`, ref: configRefs.lane },
|
|
{ id: `${path}.namespace`, ref: configRefs.namespace },
|
|
]),
|
|
requiredSpans: stringArrayField(record, "requiredSpans", path),
|
|
};
|
|
}
|
|
|
|
function kubernetesNameValue(value: string, path: string): string {
|
|
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value)) throw new Error(`${configLabel}.${path} must resolve to a Kubernetes name`);
|
|
return value;
|
|
}
|