From e8ff1e83577597c465992669a7b133f494e434bf Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:50:21 +0800 Subject: [PATCH] refactor: reuse yaml parser helpers in secrets cli (#360) Co-authored-by: Codex --- scripts/src/secrets.ts | 60 ++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/scripts/src/secrets.ts b/scripts/src/secrets.ts index 97360d61..6e793443 100644 --- a/scripts/src/secrets.ts +++ b/scripts/src/secrets.ts @@ -6,6 +6,16 @@ import { rootPath } from "./config"; import { startJob } from "./jobs"; import type { SshCaptureResult } from "./ssh"; import { capture, fingerprintValues, parseJsonOutput } from "./platform-infra-public-service"; +import { + yamlBooleanField, + yamlIntegerArrayField, + yamlIntegerField, + yamlKubernetesNameField, + yamlObjectField, + yamlRecord, + yamlStringArrayField, + yamlStringField, +} from "./platform-infra-ops-library"; const defaultConfigPath = "config/secrets-distribution.yaml"; const fieldManager = "unidesk-secret-distribution"; @@ -176,6 +186,7 @@ function parseOptions(args: string[]): SecretsOptions { function plan(options: SecretsOptions): Record { const distribution = readSecretDistributionConfig(options.configPath); + assertSelectedTargets(distribution, options); const sources = inspectSources(distribution, false); const desired = desiredSecrets(distribution, options, sources); return { @@ -199,6 +210,7 @@ function plan(options: SecretsOptions): Record { async function sync(config: UniDeskConfig, options: SecretsOptions): Promise> { const distribution = readSecretDistributionConfig(options.configPath); + assertSelectedTargets(distribution, options); if (!options.confirm || options.dryRun) { const planned = plan(options); return { ...planned, action: "secrets-sync", mode: "dry-run", mutation: false }; @@ -241,6 +253,7 @@ async function sync(config: UniDeskConfig, options: SecretsOptions): Promise> { const distribution = readSecretDistributionConfig(options.configPath); + assertSelectedTargets(distribution, options); const sources = inspectSources(distribution, false); const desired = desiredSecrets(distribution, options, sources); const perTarget = await Promise.all(groupDesiredSecretsByTarget(desired).map(async (group) => await statusTargetSecrets(config, group.target, group.secrets, options))); @@ -424,12 +437,12 @@ function inspectSources(config: SecretDistributionConfig, materialize: boolean): } function desiredSecrets(config: SecretDistributionConfig, options: SecretsOptions, sources: SourceInspection): DesiredSecret[] { + const selectedTargetIds = new Set(selectedTargets(config, options).map((target) => target.id)); const targets = new Map(config.targets.map((target) => [target.id, target])); return config.kubernetesSecrets .map((secret) => ({ secret, target: targets.get(secret.targetId) })) .filter((item): item is { secret: KubernetesSecretConfig; target: DistributionTarget } => item.target !== undefined && item.target.enabled) - .filter(({ target }) => options.scope === null || target.scope === options.scope) - .filter(({ target }) => options.targetId === null || target.id === options.targetId) + .filter(({ target }) => selectedTargetIds.has(target.id)) .map(({ secret, target }) => { const data: Record = {}; const missingKeys: DesiredSecret["missingKeys"] = []; @@ -459,6 +472,20 @@ function desiredSecrets(config: SecretDistributionConfig, options: SecretsOption }); } +function assertSelectedTargets(config: SecretDistributionConfig, options: SecretsOptions): void { + const targets = selectedTargets(config, options); + if (targets.length > 0) return; + const available = config.targets.filter((target) => target.enabled).map((target) => target.id).sort(); + throw new Error(`no enabled secrets target matches scope=${options.scope ?? "*"} target=${options.targetId ?? "*"}; available targets: ${available.length > 0 ? available.join(", ") : ""}`); +} + +function selectedTargets(config: SecretDistributionConfig, options: SecretsOptions): DistributionTarget[] { + return config.targets + .filter((target) => target.enabled) + .filter((target) => options.scope === null || target.scope === options.scope) + .filter((target) => options.targetId === null || target.id === options.targetId); +} + async function applyTargetSecrets(config: UniDeskConfig, target: DistributionTarget, secrets: DesiredSecret[], options: SecretsOptions): Promise> { if (secrets.length === 0) return { ok: true, target: targetSummary(target), mode: "skipped-no-secrets" }; const yaml = renderSecretManifest(target, secrets); @@ -751,12 +778,11 @@ function boolField(value: Record | null, key: string, fallback: } function asRecord(value: unknown, path: string): Record { - if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`); - return value as Record; + return yamlRecord(value, path); } function objectField(obj: Record, key: string, path: string): Record { - return asRecord(obj[key], `${path}.${key}`); + return yamlObjectField(obj, key, path, ""); } function arrayOfRecords(value: unknown, path: string): Record[] { @@ -765,33 +791,23 @@ function arrayOfRecords(value: unknown, path: string): Record[] } function stringField(obj: Record, key: string, path: string): string { - const value = obj[key]; - if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`); - return value.trim(); + return yamlStringField(obj, key, path, ""); } function integerField(obj: Record, key: string, path: string): number { - const value = obj[key]; - if (typeof value !== "number" || !Number.isInteger(value)) throw new Error(`${path}.${key} must be an integer`); - return value; + return yamlIntegerField(obj, key, path, ""); } function booleanField(obj: Record, key: string, path: string): boolean { - const value = obj[key]; - if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`); - return value; + return yamlBooleanField(obj, key, path, ""); } function stringArrayField(obj: Record, key: string, path: string): string[] { - const value = obj[key]; - if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) throw new Error(`${path}.${key} must be a string array`); - return value.map((item) => (item as string).trim()); + return yamlStringArrayField(obj, key, path, ""); } function numberArrayField(obj: Record, key: string, path: string): number[] { - const value = obj[key]; - if (!Array.isArray(value) || value.some((item) => typeof item !== "number" || !Number.isInteger(item))) throw new Error(`${path}.${key} must be an integer array`); - return value as number[]; + return yamlIntegerArrayField(obj, key, path, ""); } function stringMapField(obj: Record, key: string, path: string): Record { @@ -846,9 +862,7 @@ function envKeyValue(value: string, path: string): string { } function kubernetesNameField(obj: Record, key: string, path: string): string { - const value = stringField(obj, key, path); - if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(value)) throw new Error(`${path}.${key} must be a Kubernetes name`); - return value; + return yamlKubernetesNameField(obj, key, path, ""); } function kubernetesSecretKeyField(obj: Record, key: string, path: string): string {