refactor: reuse yaml parser helpers in secrets cli (#360)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
+37
-23
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user