fix: 修正 PikaOA 可观测性发现声明

This commit is contained in:
Codex
2026-07-15 05:17:40 +02:00
parent cfc7ffb071
commit 684553f9a0
6 changed files with 217 additions and 14 deletions
@@ -0,0 +1,5 @@
metricsBackend:
discovery:
labelSelector:
key: invalid label key
value: "true"
+2 -1
View File
@@ -97,8 +97,9 @@ testRuntime:
hostIP: 192.0.2.10
port: 32080
observability:
otlpEndpoint: http://otel-collector.observability.svc.cluster.local:4318
otlpEndpoint: otel-collector.observability.svc.cluster.local:4317
prometheus:
configRef: config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector
scrape: true
apiPath: /metrics
workerPath: /metrics
+3 -2
View File
@@ -117,8 +117,9 @@ testRuntime:
port: 32080
observability:
configRef: config/platform-infra/observability.yaml
otlpEndpoint: http://otel-collector.platform-infra.svc.cluster.local:4318
otlpEndpoint: otel-collector.platform-infra.svc.cluster.local:4317
prometheus:
configRef: config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector
scrape: true
apiPath: /metrics
workerPath: /metrics
@@ -228,7 +229,7 @@ delivery:
configRef: config/platform-db/postgres-pk01.yaml#exports.connectionStrings.pikaoa-database-url
observability:
configRef: config/platform-infra/observability.yaml
otlpHttpEndpoint: http://otel-collector.platform-infra.svc.cluster.local:4318
otlpEndpoint: otel-collector.platform-infra.svc.cluster.local:4317
prometheusScrape: true
propagation:
- tracecontext
@@ -0,0 +1,41 @@
# R1.5.9.10 任务报告
## 状态
- 当前状态:`in_progress`
- 关联 issue`pikasTech/unidesk#2092`
- 实现分支:`fix/pikaoa-observability`
- 本阶段仅完成源码、owning YAML、fixture 和定向验证;未执行任何运行面 mutation,未启动 Prometheus、Collector、Tempo 或 PikaOA Pod。
- 后续仍需主代理通过受控 observability apply 完成 NC01 trace/metrics 原入口验收后再决定完成状态。
## 实现
- `config/pikaoa.yaml`
- 测试与生产 `otlpEndpoint` 统一为无 scheme 的 OTLP gRPC endpoint `otel-collector.platform-infra.svc.cluster.local:4317`
- 删除生产旧字段 `otlpHttpEndpoint`,不保留兼容入口;
- 测试 Prometheus 新增 `configRef`,指向 `config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector`
- `scripts/src/pikaoa-test-target.ts`
- 复用共享 `scripts/src/ops/config-refs.ts``resolveConfigRef`
- 校验 configRef、OTLP gRPC endpoint 及 Kubernetes label key/value
- 将 selector label 加入 API/Worker Pod templateWeb 不添加;
- 在 plan/status 投影 selector 的 `sourceRef``presence``fingerprint``valuesPrinted=false`
- selector 漂移与 OTel exporter 失败均保持 `blocking=false` warning,不阻塞业务 MVP。
- fixture 与测试:
- 更新 endpoint 与 selector configRef
- 增加非法 label fixture
- 覆盖 endpoint、API/Worker selector label、Web 无 selector、configRef 缺失、引用路径错误、非法 Kubernetes label、旧生产字段删除及 `valuesPrinted=false`
## 验证
- `bun --check scripts/src/pikaoa-test-target.ts`:通过。
- `bun test scripts/src/pikaoa-test-target-async.test.ts``2 pass, 0 fail`
- `bun scripts/cli.ts check --syntax-only``11 passed, 0 failed`
- `bun scripts/cli.ts pikaoa test-target plan ...` compact text:通过,`mutation=false``valuesPrinted=false`clean Artificer workspace 缺 fixture Secret 时仅显示既有非变更 blocker,不读取或打印 Secret,也不作为本任务运行面门禁。
- `git diff --check`:通过。
- `scripts/src/pikaoa-test-target.ts`1910 行,低于 2000 行。
## 边界与后续
- 用户确认开发初期旧数据可丢;本任务不实现旧数据迁移、兼容或回滚,现有 migration 命名仅表示全新数据库初始化。
- Prometheus 当前未 apply/ready 不阻塞业务 MVP;后续由主代理按受控入口完成 apply、Tempo trace 和 Prometheus metrics 验收。
- 本报告回链 MDTODO `R1.5.9.10`,任务保持进行中。
+64 -1
View File
@@ -162,13 +162,15 @@ exit 64
return JSON.parse(result.renderedText) as Record<string, unknown>;
};
const waitFor = async (instance: string, expectedCode: string, step = "all"): Promise<Record<string, unknown>> => {
let lastStatus: Record<string, unknown> | null = null;
for (let attempt = 0; attempt < 80; attempt += 1) {
const value = await projection("status", instance, false, step);
const status = value.status as Record<string, unknown>;
if (status.code === expectedCode) return value;
lastStatus = status;
await Bun.sleep(25);
}
assert.fail(`status did not reach ${expectedCode}`);
assert.fail(`status did not reach ${expectedCode}: ${JSON.stringify(lastStatus)}`);
};
try {
@@ -178,9 +180,21 @@ exit 64
const sources = secret.sources as Array<Record<string, unknown>>;
assert.equal(sources.find((source) => source.purpose === "database.connection")?.presence, true);
const objects = plan.objects as Array<Record<string, unknown>>;
const observability = plan.observability as Record<string, unknown>;
const prometheus = observability.prometheus as Record<string, unknown>;
const selector = prometheus.selector as Record<string, unknown>;
assert.equal(objects.some((object) => object.kind === "StatefulSet" || object.name === "pikaoa-postgres"), false);
assert.equal(objects.some((object) => object.kind === "Deployment" && object.name === "pikaoa-web"), true);
assert.equal(objects.find((object) => object.kind === "Deployment" && object.name === "pikaoa-api")?.podFsGroup, 65532);
assert.equal(observability.otlpEndpoint, "otel-collector.observability.svc.cluster.local:4317");
assert.equal(selector.sourceRef, "config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector");
assert.equal(selector.presence, true);
assert.match(String(selector.fingerprint), /^sha256:[0-9a-f]{64}$/u);
assert.equal(selector.valuesPrinted, false);
for (const component of ["pikaoa-api", "pikaoa-worker"]) {
assert.equal(objects.find((object) => object.kind === "Deployment" && object.name === component)?.prometheusSelectorLabel, selector.value);
}
assert.equal(objects.find((object) => object.kind === "Deployment" && object.name === "pikaoa-web")?.prometheusSelectorLabel, undefined);
assert.deepEqual(plan.exposure, { serviceType: "NodePort", serviceName: "pikaoa-web", hostIP: "192.0.2.10", port: 32080 });
assert.deepEqual(plan.webApiUpstream, { envName: "PIKAOA_API_UPSTREAM", value: "http://pikaoa-api:8080", scope: "same-namespace" });
@@ -387,6 +401,7 @@ test("validation-only fixture never invokes remote capture", async () => {
valuesPrinted: false,
});
assert.equal(planPayload.valuesPrinted, false);
assert.equal(((planPayload.observability as Record<string, unknown>).prometheus as Record<string, unknown>).selector !== undefined, true);
assert.equal(JSON.stringify(planPayload).includes("fixture-db-password"), false);
assert.equal(JSON.stringify(planPayload).includes("postgres://"), false);
@@ -407,6 +422,54 @@ test("validation-only fixture never invokes remote capture", async () => {
rmSync(missingSchemaRoot, { recursive: true, force: true });
}
const missingSelectorRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-missing-selector-"));
const missingSelectorPath = join(missingSelectorRoot, "pikaoa.yaml");
const fixture = readFileSync(rootPath("config", "fixtures", "pikaoa-test-target.yaml"), "utf8");
writeFileSync(missingSelectorPath, fixture.replace(" configRef: config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector\n", ""));
try {
await assert.rejects(
runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", missingSelectorPath, "--target", "TEST01", "--instance", "missing-selector", "--output", "json"], forbiddenCapture),
/observability\.prometheus\.configRef /u,
);
} finally {
rmSync(missingSelectorRoot, { recursive: true, force: true });
}
const invalidSelectorRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-invalid-selector-"));
const invalidSelectorPath = join(invalidSelectorRoot, "pikaoa.yaml");
writeFileSync(invalidSelectorPath, fixture.replace(
"config/platform-infra/observability.yaml#metricsBackend.discovery.labelSelector",
"config/fixtures/pikaoa-observability-invalid-label.yaml#metricsBackend.discovery.labelSelector",
));
try {
await assert.rejects(
runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", invalidSelectorPath, "--target", "TEST01", "--instance", "invalid-selector", "--output", "json"], forbiddenCapture),
/observability\.prometheus\.configRef\.key Kubernetes label key/u,
);
} finally {
rmSync(invalidSelectorRoot, { recursive: true, force: true });
}
const missingSelectorPathRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-missing-selector-path-"));
const missingSelectorConfigPath = join(missingSelectorPathRoot, "pikaoa.yaml");
writeFileSync(missingSelectorConfigPath, fixture.replace("metricsBackend.discovery.labelSelector", "metricsBackend.discovery.missingSelector"));
try {
await assert.rejects(
runPikaoaCommand({} as UniDeskConfig, ["test-target", "status", "--config", missingSelectorConfigPath, "--target", "TEST01", "--instance", "missing-selector-path", "--output", "json"], forbiddenCapture),
/observability\.prometheus\.configRef .*missingSelector/u,
);
} finally {
rmSync(missingSelectorPathRoot, { recursive: true, force: true });
}
const owningConfig = Bun.YAML.parse(readFileSync(rootPath("config", "pikaoa.yaml"), "utf8")) as Record<string, unknown>;
const testTarget = (((owningConfig.testRuntime as Record<string, unknown>).targets as Record<string, unknown>).NC01 as Record<string, unknown>);
assert.equal((testTarget.observability as Record<string, unknown>).otlpEndpoint, "otel-collector.platform-infra.svc.cluster.local:4317");
const production = (((owningConfig.delivery as Record<string, unknown>).targets as Record<string, unknown>).NC01 as Record<string, unknown>);
const productionObservability = (production.runtime as Record<string, unknown>).observability as Record<string, unknown>;
assert.equal(productionObservability.otlpEndpoint, "otel-collector.platform-infra.svc.cluster.local:4317");
assert.equal("otlpHttpEndpoint" in productionObservability, false);
const unconfiguredRoot = mkdtempSync(join(tmpdir(), "pikaoa-test-target-unconfigured-"));
const unconfiguredPath = join(unconfiguredRoot, "pikaoa.yaml");
writeFileSync(unconfiguredPath, "version: 1\nkind: pikaoa-platform-delivery\ntestRuntime:\n defaultTargetId: null\n targets: {}\n");
+102 -10
View File
@@ -9,6 +9,7 @@ import { renderMachine } from "./cicd-render";
import type { RenderedCliResult } from "./output";
import { CliInputError } from "./output";
import { capture, compactCapture, parseJsonOutput, readYamlRecord, sha256Fingerprint, shQuote } from "./platform-infra-ops-library";
import { resolveConfigRef } from "./ops/config-refs";
type TestTargetAction = "plan" | "status" | "start" | "stop";
type TestTargetStep = "all" | "foundation" | "migration" | "api" | "worker" | "web";
@@ -31,6 +32,15 @@ interface SecretSourceSpec {
targetKey: string;
}
interface PrometheusSelectorSpec {
sourceRef: string;
key: string;
value: string;
presence: true;
fingerprint: string;
valuesPrinted: false;
}
interface TestTargetSpec {
id: string;
enabled: boolean;
@@ -107,6 +117,7 @@ interface TestTargetSpec {
observability: {
otlpEndpoint: string;
prometheusScrape: boolean;
prometheusSelector: PrometheusSelectorSpec;
apiMetricsPath: string;
workerMetricsPath: string;
exporterWarning: boolean;
@@ -387,8 +398,9 @@ function parseTarget(id: string, root: Record<string, unknown>, configLabel: str
port: positiveInteger(exposure.port, `${path}.exposure.port`, 65_535),
},
observability: {
otlpEndpoint: nonEmpty(observability.otlpEndpoint, `${path}.observability.otlpEndpoint`),
otlpEndpoint: otlpGrpcEndpoint(observability.otlpEndpoint, `${path}.observability.otlpEndpoint`),
prometheusScrape: boolean(prometheus.scrape, `${path}.observability.prometheus.scrape`),
prometheusSelector: prometheusSelector(prometheus.configRef, `${path}.observability.prometheus`),
apiMetricsPath: httpPath(prometheus.apiPath, `${path}.observability.prometheus.apiPath`),
workerMetricsPath: httpPath(prometheus.workerPath, `${path}.observability.prometheus.workerPath`),
exporterWarning: boolean(exporterFailure.warning, `${path}.observability.exporterFailure.warning`),
@@ -439,6 +451,7 @@ function planPayload(options: TestTargetOptions, selection: Selection): Record<s
missingFields: missingMutationFields(options, selection),
blockers,
warnings,
observability: observabilitySummary(selection.target),
plan: renderWorkloads && context !== null ? renderedPlan(selection, context) : null,
next: nextCommands(options, selection.target),
};
@@ -482,7 +495,7 @@ function renderedPlan(selection: Selection, context: RenderContext): Record<stri
ttlSeconds: target.ttlSeconds,
cleanup: target.cleanup,
},
objects: structuralManifest.map(objectSummary),
objects: structuralManifest.map((object) => objectSummary(object, target.observability.prometheusSelector.key)),
selectors,
secret: {
name: target.runtime.secretName,
@@ -504,8 +517,7 @@ function renderedPlan(selection: Selection, context: RenderContext): Record<stri
web: { liveness: target.probes.webLivenessPath, readiness: target.probes.webReadinessPath },
},
observability: {
otlpEndpoint: target.observability.otlpEndpoint,
prometheusScrape: target.observability.prometheusScrape,
...observabilitySummary(target),
exporterFailure: { warning: target.observability.exporterWarning, blocking: false },
},
exposure: { serviceType: target.exposure.serviceType, serviceName: target.exposure.serviceName, hostIP: target.exposure.hostIP, port: target.exposure.port },
@@ -670,6 +682,41 @@ function consistencyWarnings(target: TestTargetSpec, context: RenderContext | nu
return warnings;
}
function observabilitySummary(target: TestTargetSpec): Record<string, unknown> {
return {
otlpEndpoint: target.observability.otlpEndpoint,
prometheus: {
scrape: target.observability.prometheusScrape,
selector: target.observability.prometheusSelector,
},
valuesPrinted: false,
};
}
function prometheusRuntimeStatus(items: Record<string, unknown>[], selector: PrometheusSelectorSpec, runtimePresent: boolean, step: TestTargetStep): Record<string, unknown> {
const components = step === "all" ? ["api", "worker"] : step === "api" || step === "worker" ? [step] : [];
const workloads = components.map((component) => {
const deployment = items.find((item) => item.kind === "Deployment" && optionalRecord(item.metadata, "resource.metadata")?.name === `pikaoa-${component}`);
const spec = deployment === undefined ? null : optionalRecord(deployment.spec, "resource.spec");
const template = spec === null ? null : optionalRecord(spec.template, "resource.spec.template");
const metadata = template === null ? null : optionalRecord(template.metadata, "resource.spec.template.metadata");
const labels = metadata === null ? null : optionalRecord(metadata.labels, "resource.spec.template.metadata.labels");
const actual = labels?.[selector.key];
return { component, present: deployment !== undefined, actual: typeof actual === "string" ? actual : null, matches: actual === selector.value };
});
const checked = runtimePresent && components.length > 0;
const drift = checked && workloads.some((workload) => !workload.matches);
return {
selector,
checked,
workloads,
drift,
warning: drift ? warning("prometheus-selector-label-drift", "API/Worker Pod template 的 Prometheus selector label 与 owning configRef 不一致;该漂移不阻塞业务运行。") : null,
blocking: false,
valuesPrinted: false,
};
}
function buildManifest(target: TestTargetSpec, context: RenderContext, secrets: SecretMaterial): Record<string, unknown>[] {
const labels = ownershipLabels(target, context);
const secretData = runtimeSecretData(target, context, secrets);
@@ -677,6 +724,9 @@ function buildManifest(target: TestTargetSpec, context: RenderContext, secrets:
const workerSelector = componentSelector("worker", context.instanceId);
const webSelector = componentSelector("web", context.instanceId);
const migrationSelector = componentSelector("migration", context.instanceId);
const prometheusPodLabels = target.observability.prometheusScrape ? {
[target.observability.prometheusSelector.key]: target.observability.prometheusSelector.value,
} : {};
const commonPodAnnotations = (path: string, port: number): Record<string, string> => target.observability.prometheusScrape ? {
"prometheus.io/scrape": "true",
"prometheus.io/path": path,
@@ -728,7 +778,7 @@ function buildManifest(target: TestTargetSpec, context: RenderContext, secrets:
},
},
},
deployment(target, context, "api", context.apiImage, apiSelector, target.runtime.apiPort, commonPodAnnotations(target.observability.apiMetricsPath, target.runtime.apiPort), [
deployment(target, context, "api", context.apiImage, apiSelector, target.runtime.apiPort, prometheusPodLabels, commonPodAnnotations(target.observability.apiMetricsPath, target.runtime.apiPort), [
{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" },
{ name: "PIKAOA_SESSION_SECRET", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.runtime.sessionSecret.targetKey } } },
{ name: "PIKAOA_BOOTSTRAP_ADMIN_USERNAME", value: target.runtime.administrator.username },
@@ -736,10 +786,10 @@ function buildManifest(target: TestTargetSpec, context: RenderContext, secrets:
{ name: "PIKAOA_BOOTSTRAP_EMPLOYEE_USERNAME", value: target.runtime.employee.username },
{ name: "PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.runtime.employee.password.targetKey } } },
], target.probes.apiLivenessPath, target.probes.apiReadinessPath),
deployment(target, context, "worker", context.workerImage, workerSelector, target.runtime.workerMetricsPort, commonPodAnnotations(target.observability.workerMetricsPath, target.runtime.workerMetricsPort), [
deployment(target, context, "worker", context.workerImage, workerSelector, target.runtime.workerMetricsPort, prometheusPodLabels, commonPodAnnotations(target.observability.workerMetricsPath, target.runtime.workerMetricsPort), [
{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" },
], target.probes.workerLivenessPath, target.probes.workerReadinessPath),
deployment(target, context, "web", context.webImage, webSelector, target.runtime.webPort, {}, [
deployment(target, context, "web", context.webImage, webSelector, target.runtime.webPort, {}, {}, [
{ name: target.runtime.webApiUpstreamEnv, value: `http://${target.runtime.apiServiceName}:${target.runtime.apiPort}` },
], target.probes.webLivenessPath, target.probes.webReadinessPath),
];
@@ -765,6 +815,7 @@ function deployment(
image: string,
selector: Record<string, string>,
port: number,
podLabels: Record<string, string>,
annotations: Record<string, string>,
env: Array<Record<string, unknown>>,
livenessPath: string,
@@ -776,7 +827,7 @@ function deployment(
spec: {
replicas: 1, selector: { matchLabels: selector },
template: {
metadata: { labels: { ...labels, ...selector }, annotations },
metadata: { labels: { ...labels, ...selector, ...podLabels }, annotations },
spec: {
...(component === "api" ? { securityContext: { fsGroup: target.runtime.attachment.fsGroup } } : {}),
containers: [{
@@ -1419,6 +1470,7 @@ function summarizeRemoteStatus(parsed: Record<string, unknown>, target: TestTarg
const labels = optionalRecord(metadata.labels, "status.namespace.metadata.labels") ?? {};
const resources = optionalRecord(runtime.resources, "status.runtime.resources") ?? {};
const items = Array.isArray(resources.items) ? resources.items.filter(isRecord) : [];
const prometheus = prometheusRuntimeStatus(items, target.observability.prometheusSelector, runtime.exists === true, context.step);
return {
ok: parsed.ok === true,
code: parsed.code ?? "start-status-unknown",
@@ -1453,7 +1505,7 @@ function summarizeRemoteStatus(parsed: Record<string, unknown>, target: TestTarg
}),
database: { external: true, configRef: target.database.configRef, ...secretSourceSummary(configPath, target.database.connection) },
exposure: { hostIP: target.exposure.hostIP, port: target.exposure.port, serviceName: target.exposure.serviceName },
observability: { otlpEndpoint: target.observability.otlpEndpoint, prometheusScrape: target.observability.prometheusScrape, blocking: false },
observability: { ...observabilitySummary(target), prometheus, blocking: false },
},
valuesPrinted: false,
};
@@ -1566,17 +1618,20 @@ function namespaceAnnotations(target: TestTargetSpec, context: RenderContext): R
};
}
function objectSummary(object: Record<string, unknown>): Record<string, unknown> {
function objectSummary(object: Record<string, unknown>, prometheusSelectorKey: string): Record<string, unknown> {
const metadata = optionalRecord(object.metadata, "metadata") ?? {};
const labels = optionalRecord(metadata.labels, "metadata.labels") ?? {};
const spec = optionalRecord(object.spec, "spec") ?? {};
const template = optionalRecord(spec.template, "spec.template") ?? {};
const templateMetadata = optionalRecord(template.metadata, "spec.template.metadata") ?? {};
const podLabels = optionalRecord(templateMetadata.labels, "spec.template.metadata.labels") ?? {};
const podSpec = optionalRecord(template.spec, "spec.template.spec") ?? {};
const securityContext = optionalRecord(podSpec.securityContext, "spec.template.spec.securityContext") ?? {};
return {
kind: object.kind ?? null,
name: metadata.name ?? null,
owned: labels["app.kubernetes.io/managed-by"] === MANAGED_BY && labels[TEST_RUNTIME_LABEL] === "true" && typeof labels[TARGET_LABEL] === "string" && typeof labels[INSTANCE_LABEL] === "string",
prometheusSelectorLabel: object.kind === "Deployment" && typeof podLabels[prometheusSelectorKey] === "string" ? podLabels[prometheusSelectorKey] : undefined,
podFsGroup: securityContext.fsGroup,
secretValuesRedacted: object.kind === "Secret" ? true : undefined,
};
@@ -1705,6 +1760,43 @@ function imageReference(value: unknown, path: string): string {
return image;
}
function otlpGrpcEndpoint(value: unknown, path: string): string {
const endpoint = nonEmpty(value, path);
if (endpoint.includes("://") || !/^[A-Za-z0-9.-]+:[1-9][0-9]{0,4}$/u.test(endpoint)) throw new Error(`${path} scheme OTLP gRPC host:port`);
const port = Number(endpoint.slice(endpoint.lastIndexOf(":") + 1));
if (port > 65_535) throw new Error(`${path} 端口必须是 1-65535`);
return endpoint;
}
function prometheusSelector(value: unknown, path: string): PrometheusSelectorSpec {
const sourceRef = nonEmpty(value, `${path}.configRef`);
let resolved: ReturnType<typeof resolveConfigRef>;
try {
resolved = resolveConfigRef(sourceRef, `${path}.configRef`);
} catch (error) {
throw new Error(`${path}.configRef 解析失败:${error instanceof Error ? error.message : String(error)}`);
}
const selector = record(resolved.value, `${path}.configRef (${sourceRef})`);
const key = kubernetesLabelKey(nonEmpty(selector.key, `${path}.configRef.key`), `${path}.configRef.key`);
const selectorValue = kubernetesLabelValue(nonEmpty(selector.value, `${path}.configRef.value`), `${path}.configRef.value`);
return { sourceRef, key, value: selectorValue, presence: true, fingerprint: `sha256:${resolved.sha256}`, valuesPrinted: false };
}
function kubernetesLabelKey(value: string, path: string): string {
const parts = value.split("/");
const name = parts.pop() ?? "";
const prefix = parts.length === 1 ? parts[0] : null;
const labelNamePattern = /^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?$/u;
const dnsPrefixPattern = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/u;
if (parts.length > 1 || !labelNamePattern.test(name) || (prefix !== null && !dnsPrefixPattern.test(prefix))) throw new Error(`${path} 必须是合法 Kubernetes label key`);
return value;
}
function kubernetesLabelValue(value: string, path: string): string {
if (!/^[A-Za-z0-9](?:[A-Za-z0-9_.-]{0,61}[A-Za-z0-9])?$/u.test(value)) throw new Error(`${path} Kubernetes label value`);
return value;
}
function postgresIdentifier(value: unknown, path: string): string {
const name = nonEmpty(value, path);
if (!/^[a-z_][a-z0-9_]{0,62}$/u.test(name)) throw new Error(`${path} PostgreSQL identifier`);