refactor: reuse yaml parser helpers in secrets cli (#360)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-14 12:50:21 +08:00
committed by GitHub
parent f208e3afcf
commit e8ff1e8357
+37 -23
View File
@@ -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<string, unknown> {
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<string, unknown> {
async function sync(config: UniDeskConfig, options: SecretsOptions): Promise<Record<string, unknown>> {
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<Rec
async function status(config: UniDeskConfig, options: SecretsOptions): Promise<Record<string, unknown>> {
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<string, string> = {};
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(", ") : "<none>"}`);
}
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<Record<string, unknown>> {
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<string, unknown> | null, key: string, fallback:
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`);
return value as Record<string, unknown>;
return yamlRecord(value, path);
}
function objectField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown> {
return asRecord(obj[key], `${path}.${key}`);
return yamlObjectField(obj, key, path, "");
}
function arrayOfRecords(value: unknown, path: string): Record<string, unknown>[] {
@@ -765,33 +791,23 @@ function arrayOfRecords(value: unknown, path: string): Record<string, unknown>[]
}
function stringField(obj: Record<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, 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<string, unknown>, key: string, path: string): Record<string, string> {
@@ -846,9 +862,7 @@ function envKeyValue(value: string, path: string): string {
}
function kubernetesNameField(obj: Record<string, unknown>, 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<string, unknown>, key: string, path: string): string {