fix: 严格渲染 Prometheus 选择器

This commit is contained in:
Codex
2026-07-13 21:35:36 +02:00
parent ef3b5fde28
commit c3762b310c
3 changed files with 52 additions and 7 deletions
@@ -4,8 +4,9 @@ import { rootPath } from "./config";
import { isRenderedCliResult } from "./output";
import { runPlatformObservabilityCommand } from "./platform-infra-observability";
import { resolveTarget } from "./platform-infra-observability/actions";
import { readObservabilityConfig } from "./platform-infra-observability/config";
import { isKubernetesLabelValue, isKubernetesQualifiedName, readObservabilityConfig } from "./platform-infra-observability/config";
import { renderObservabilityPlanFailure } from "./platform-infra-observability/plan-render";
import { prometheusMetaLabel, re2Literal } from "./platform-infra-observability/prometheus";
import { statusScript } from "./platform-infra-observability/search-script";
import { renderManifest } from "./platform-infra-observability/trace-script";
@@ -77,12 +78,32 @@ describe("platform-infra observability progressive disclosure", () => {
const explicitPath = "source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]\n action: replace\n target_label: __metrics_path__\n regex: (/.+)";
const defaultPath = "source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]\n action: replace\n target_label: __metrics_path__\n regex: ^$\n replacement: /metrics";
expect(manifest).toContain("source_labels: [__meta_kubernetes_pod_label_unidesk_ai_metrics]");
expect(manifest).toContain('regex: "true"');
expect(manifest).toContain("regex: '^true$'");
expect(manifest).toContain(explicitPath);
expect(manifest).toContain(defaultPath);
expect(manifest.indexOf(explicitPath)).toBeLessThan(manifest.indexOf(defaultPath));
});
test("Prometheus selector rendering uses qualified names, stable meta labels and literal RE2", () => {
const observability = readObservabilityConfig();
observability.metricsBackend.discovery.labelSelector = {
key: "metrics.example.com/release-track",
value: "v1.2-beta",
};
observability.metricsBackend.discovery.annotationKeys.scrape = "prometheus.io/scrape-enabled";
const manifest = renderManifest(observability, resolveTarget(observability, "NC01"));
expect(manifest).toContain("__meta_kubernetes_pod_label_metrics_example_com_release_track");
expect(manifest).toContain("__meta_kubernetes_pod_annotation_prometheus_io_scrape_enabled");
expect(manifest).toContain("regex: '^v1\\.2-beta$'");
expect(prometheusMetaLabel("a.b/c-d_e")).toBe("a_b_c_d_e");
expect(re2Literal("v1.2-beta")).toBe("^v1\\.2-beta$");
expect(isKubernetesQualifiedName("metrics.example.com/release-track")).toBe(true);
expect(isKubernetesQualifiedName("metrics.example.com/release/track")).toBe(false);
expect(isKubernetesQualifiedName("Metrics.example.com/release-track")).toBe(false);
expect(isKubernetesQualifiedName(`${"a".repeat(64)}.example/release-track`)).toBe(false);
expect(isKubernetesLabelValue("v1.2-beta")).toBe(true);
});
test("query commands expose only scoped help", () => {
for (const operation of ["search", "trace", "diagnose-code-agent", "metrics-query"]) {
const result = cli(["platform-infra", "observability", operation, "--help"]);
@@ -149,8 +149,8 @@ export function readObservabilityConfig(): ObservabilityConfig {
for (const targetId of config.metricsBackend.targetIds) assertKnownEnabledTarget(config.targets, targetId, "metricsBackend.targetIds");
if (config.metricsBackend.enabled && config.metricsBackend.targetIds.length === 0) throw new Error(`${configLabel}.metricsBackend.targetIds must not be empty when metricsBackend.enabled is true`);
if (new Set(config.metricsBackend.targetIds.map((targetId) => targetId.toLowerCase())).size !== config.metricsBackend.targetIds.length) throw new Error(`${configLabel}.metricsBackend.targetIds must not contain duplicates`);
if (!/^[A-Za-z0-9]([A-Za-z0-9._/-]*[A-Za-z0-9])?$/u.test(config.metricsBackend.discovery.labelSelector.key)) throw new Error(`${configLabel}.metricsBackend.discovery.labelSelector.key must be a Kubernetes label key`);
if (!/^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)?$/u.test(config.metricsBackend.discovery.labelSelector.value) || config.metricsBackend.discovery.labelSelector.value.length > 63) throw new Error(`${configLabel}.metricsBackend.discovery.labelSelector.value must be a Kubernetes label value`);
if (!isKubernetesQualifiedName(config.metricsBackend.discovery.labelSelector.key)) throw new Error(`${configLabel}.metricsBackend.discovery.labelSelector.key must be a Kubernetes qualified name`);
if (!isKubernetesLabelValue(config.metricsBackend.discovery.labelSelector.value)) throw new Error(`${configLabel}.metricsBackend.discovery.labelSelector.value must be a Kubernetes label value`);
if (config.collector.replicas < 0 || config.traceBackend.replicas < 0 || config.metricsBackend.replicas < 0) throw new Error(`${configLabel} replicas must be >= 0`);
if (config.metricsBackend.query.timeoutSeconds < 1 || config.metricsBackend.query.maxSeries < 1 || config.metricsBackend.query.maxResponseBytes < 1024) throw new Error(`${configLabel}.metricsBackend.query budgets must be positive and maxResponseBytes must be >= 1024`);
if (config.sampling.ratio < 0 || config.sampling.ratio > 1) throw new Error(`${configLabel}.sampling.ratio must be between 0 and 1`);
@@ -158,6 +158,25 @@ export function readObservabilityConfig(): ObservabilityConfig {
return config;
}
export function isKubernetesQualifiedName(value: string): boolean {
const slash = value.indexOf("/");
if (slash !== value.lastIndexOf("/")) return false;
const prefix = slash < 0 ? null : value.slice(0, slash);
const name = slash < 0 ? value : value.slice(slash + 1);
if (!isKubernetesNamePart(name)) return false;
if (prefix === null) return true;
if (prefix.length === 0 || prefix.length > 253) return false;
return prefix.split(".").every((segment) => segment.length <= 63 && /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(segment));
}
export function isKubernetesLabelValue(value: string): boolean {
return value.length <= 63 && (value === "" || isKubernetesNamePart(value));
}
function isKubernetesNamePart(value: string): boolean {
return value.length > 0 && value.length <= 63 && /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/u.test(value);
}
export function imageSpec(record: Record<string, unknown>, path: string): ImageSpec {
const image = {
repository: stringField(record, "repository", path),
@@ -14,7 +14,7 @@ export function prometheusManifests(observability: ObservabilityConfig, target:
const namespaceNames = prometheus.discovery.namespaces.length > 0
? `\n namespaces:\n names:\n${prometheus.discovery.namespaces.map((name) => ` - ${name}`).join("\n")}`
: "";
const config = `global:\n scrape_interval: ${prometheus.discovery.scrapeInterval}\n scrape_timeout: ${prometheus.discovery.scrapeTimeout}\nscrape_configs:\n - job_name: kubernetes-pods\n kubernetes_sd_configs:\n - role: pod${namespaceNames}\n relabel_configs:\n - source_labels: [__meta_kubernetes_pod_annotation_${annotationMeta(prometheus.discovery.annotationKeys.scrape)}]\n action: keep\n regex: \"true\"\n - source_labels: [__meta_kubernetes_pod_label_${labelMeta(prometheus.discovery.labelSelector.key)}]\n action: keep\n regex: \"${prometheus.discovery.labelSelector.value}\"\n - source_labels: [__meta_kubernetes_pod_annotation_${annotationMeta(prometheus.discovery.annotationKeys.path)}]\n action: replace\n target_label: __metrics_path__\n regex: (/.+)\n - source_labels: [__address__, __meta_kubernetes_pod_annotation_${annotationMeta(prometheus.discovery.annotationKeys.port)}]\n action: replace\n regex: ([^:]+)(?::\\d+)?;(\\d+)\n replacement: \$1:\$2\n target_label: __address__\n - source_labels: [__meta_kubernetes_pod_annotation_${annotationMeta(prometheus.discovery.annotationKeys.path)}]\n action: replace\n target_label: __metrics_path__\n regex: ^$\n replacement: ${prometheus.discovery.defaultPath}\n`;
const config = `global:\n scrape_interval: ${prometheus.discovery.scrapeInterval}\n scrape_timeout: ${prometheus.discovery.scrapeTimeout}\nscrape_configs:\n - job_name: kubernetes-pods\n kubernetes_sd_configs:\n - role: pod${namespaceNames}\n relabel_configs:\n - source_labels: [__meta_kubernetes_pod_annotation_${prometheusMetaLabel(prometheus.discovery.annotationKeys.scrape)}]\n action: keep\n regex: \"true\"\n - source_labels: [__meta_kubernetes_pod_label_${prometheusMetaLabel(prometheus.discovery.labelSelector.key)}]\n action: keep\n regex: '${re2Literal(prometheus.discovery.labelSelector.value)}'\n - source_labels: [__meta_kubernetes_pod_annotation_${prometheusMetaLabel(prometheus.discovery.annotationKeys.path)}]\n action: replace\n target_label: __metrics_path__\n regex: (/.+)\n - source_labels: [__address__, __meta_kubernetes_pod_annotation_${prometheusMetaLabel(prometheus.discovery.annotationKeys.port)}]\n action: replace\n regex: ([^:]+)(?::\\d+)?;(\\d+)\n replacement: \$1:\$2\n target_label: __address__\n - source_labels: [__meta_kubernetes_pod_annotation_${prometheusMetaLabel(prometheus.discovery.annotationKeys.path)}]\n action: replace\n target_label: __metrics_path__\n regex: ^$\n replacement: ${prometheus.discovery.defaultPath}\n`;
const labels = ` app.kubernetes.io/name: ${prometheus.deploymentName}\n app.kubernetes.io/component: metrics-backend\n app.kubernetes.io/part-of: platform-infra\n app.kubernetes.io/managed-by: unidesk`;
return [
`apiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: ${prometheus.serviceAccountName}\n namespace: ${target.namespace}\n labels:\n${labels}\n`,
@@ -44,5 +44,10 @@ export async function metricsQuery(config: UniDeskConfig, options: MetricsQueryO
return { ok, command: "platform-infra observability metrics-query", contentType: "text/plain", renderedText: [`platform-infra observability metrics-query (${ok ? "ok" : "not-ok"})`, "", `target=${target.id} prometheus=${prometheus.serviceName}.${target.namespace}.svc.cluster.local:${prometheus.httpPort}`, `query=${options.query}`, `budget=timeout:${prometheus.query.timeoutSeconds}s series:${prometheus.query.maxSeries} bytes:${prometheus.query.maxResponseBytes} truncated=${textValue(parsed?.truncated)}`, "", formatTable(["RESULT"], rows.length > 0 ? rows : [["-"]]), "", `Next: bun scripts/cli.ts platform-infra observability metrics-query --target ${target.id} --query ${shQuote(options.query)} --full`].join("\n") };
}
function annotationMeta(value: string): string { return value.replaceAll(".", "_").replaceAll("/", "_"); }
function labelMeta(value: string): string { return value.replaceAll(".", "_").replaceAll("/", "_").replaceAll("-", "_"); }
export function prometheusMetaLabel(value: string): string {
return value.replace(/[^A-Za-z0-9_]/gu, "_");
}
export function re2Literal(value: string): string {
return `^${value.replace(/[\\.^$|?*+()[\]{}]/gu, "\\$&")}$`;
}