// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. runtime-secrets module for scripts/src/artifact-registry.ts. // Moved mechanically from scripts/src/artifact-registry.ts:2232-2390 for #903. import { createHash } from "node:crypto"; import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { runCommand, type CommandResult } from "../command"; import { readConfig, type UniDeskConfig, repoRoot, rootPath } from "../config"; import { startJob } from "../jobs"; import { compareDeployJsonExecutorMirrors, deployJsonCommitImage, deployJsonDriftResult, deployJsonSourceOfTruth, hasDeployJsonExecutorContract, k3sManifestExecutorMirror, parseDeployJsonServiceContractBase64, readDeployJsonServiceContractFromFile, type DeployJsonExecutorMirror, type DeployJsonServiceContract, } from "../deploy-json-contract"; import { d601K3sGuardShellLines } from "../d601-k3s-guard"; import { composeRuntimeEnvValue } from "../runtime-env"; import type { ArtifactConsumerTarget, ArtifactDeployEnvironment, ArtifactRegistryOptions, AuthHealthGate, ComposeArtifactRuntime, RuntimeSecretContract, RuntimeSecretPresence, RuntimeSecretRequirement, RuntimeSecretSource } from "./types"; import { composeFile, safeName, shellQuote } from "./bundle"; import { baiduNetdiskAuthHealthGate } from "./catalog"; import { runtimeSecretPresenceFromEnvText } from "./install"; import { combineCommandResults, downloadRemoteFile, runRemoteScript, runRemoteScriptBackground } from "./remote"; export function baiduNetdiskAuthHealthGateStatus(health: unknown): { ok: boolean; requiredFields: string[]; failedFields: string[] } { const record = typeof health === "object" && health !== null && !Array.isArray(health) ? health as Record : {}; const auth = typeof record.auth === "object" && record.auth !== null && !Array.isArray(record.auth) ? record.auth as Record : {}; const failedFields = baiduNetdiskAuthHealthGate.requiredFields.filter((field) => auth[field] !== true); return { ok: failedFields.length === 0, requiredFields: baiduNetdiskAuthHealthGate.requiredFields, failedFields, }; } export function composeArtifactRuntime(config: UniDeskConfig, target: ArtifactConsumerTarget): ComposeArtifactRuntime { if (target.compose === undefined) throw new Error("compose artifact target missing compose config"); const compose = target.compose; const workDir = compose.workDir ?? config.providerGateway.upgrade.hostProjectRoot; const composeFile = compose.composeFile ?? config.providerGateway.upgrade.composeFile; const composeEnvFile = config.providerGateway.upgrade.composeEnvFile; const envFile = join(workDir, composeEnvFile); const project = compose.projectHint ?? (config.providerGateway.upgrade.composeProject || config.docker.projectName); const command = ["docker", "compose", "--env-file", envFile, "-f", join(workDir, composeFile), "-p", project]; return { workDir, composeFile, composeEnvFile, envFile, project, command }; } export function runtimeSecretSourceForTarget(config: UniDeskConfig, target: ArtifactConsumerTarget): RuntimeSecretSource { const runtime = composeArtifactRuntime(config, target); return { kind: "compose-env-file", path: runtime.envFile, exists: existsSync(runtime.envFile), configSource: "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile", workDir: runtime.workDir, composeEnvFile: runtime.composeEnvFile, composeService: target.compose?.serviceName ?? null, containerName: target.compose?.containerName ?? null, valuesPrinted: false, }; } export function runtimeSecretRecommendedAction(missing: string[]): string { if (missing.length === 0) return "none"; return "Restore the missing source env keys in the canonical Compose env file without printing values, then rerun deploy apply --env --service --dry-run before any live apply."; } export function runtimeSecretRecommendedActionForService(contract: RuntimeSecretContract, environment: ArtifactDeployEnvironment, serviceId: string): RuntimeSecretContract { if (contract.requiredSecretsPresent) return { ...contract, recommendedAction: "none" }; return { ...contract, recommendedAction: `Restore ${contract.missingSecretKeys.join(", ")} in the canonical Compose env file without printing values, then rerun deploy apply --env ${environment} --service ${serviceId} --dry-run before any live apply.`, }; } export function runtimeSecretContractFromPresence( source: RuntimeSecretSource, requirements: RuntimeSecretRequirement[], presence: RuntimeSecretPresence[], ): RuntimeSecretContract { const missingSecretKeys = presence.filter((item) => !item.present).map((item) => item.sourceEnvName); const requiredSecretsPresent = requirements.length === 0 || (source.exists && missingSecretKeys.length === 0); return { check: "runtime-secret-presence", secretSource: source, requiredSecretsPresent, missingSecretKeys, recommendedAction: runtimeSecretRecommendedAction(missingSecretKeys), valuesPrinted: false, requirements: presence.map((item) => ({ sourceEnvName: item.sourceEnvName, containerEnvName: item.containerEnvName, present: item.present, valuePrinted: false, })), dryRunDisposition: requirements.length === 0 ? "not-required" : requiredSecretsPresent ? "ready-for-live-apply" : "secret-source-blocked", }; } export function runtimeSecretContractFromEnvText( envText: string, requirements: RuntimeSecretRequirement[], sourceOverrides: Partial> = {}, ): RuntimeSecretContract { const source: RuntimeSecretSource = { kind: "compose-env-file", path: sourceOverrides.path ?? "", exists: sourceOverrides.exists ?? true, configSource: sourceOverrides.configSource ?? "config.providerGateway.upgrade.hostProjectRoot + config.providerGateway.upgrade.composeEnvFile", workDir: sourceOverrides.workDir ?? "", composeEnvFile: sourceOverrides.composeEnvFile ?? "", composeService: sourceOverrides.composeService ?? null, containerName: sourceOverrides.containerName ?? null, valuesPrinted: false, }; return runtimeSecretContractFromPresence(source, requirements, runtimeSecretPresenceFromEnvText(envText, requirements)); } export function runtimeSecretContractForComposeTarget( config: UniDeskConfig, target: ArtifactConsumerTarget, requirements: RuntimeSecretRequirement[] | undefined, ): RuntimeSecretContract | undefined { if (requirements === undefined) return undefined; const source = runtimeSecretSourceForTarget(config, target); const envText = source.exists ? readFileSync(source.path, "utf8") : ""; return runtimeSecretContractFromPresence(source, requirements, runtimeSecretPresenceFromEnvText(envText, requirements)); } export function composeArtifactSecretPreflight(envFile: string, requirements: RuntimeSecretRequirement[] | undefined): { ok: boolean; envFile: string; requirements: RuntimeSecretPresence[]; missing: RuntimeSecretPresence[]; valuesLogged: false } { const requirementList = requirements ?? []; const envText = existsSync(envFile) ? readFileSync(envFile, "utf8") : ""; const presence = runtimeSecretPresenceFromEnvText(envText, requirementList); const missing = presence.filter((item) => !item.present); return { ok: requirementList.length === 0 || (existsSync(envFile) && missing.length === 0), envFile, requirements: presence, missing, valuesLogged: false, }; } export function authHealthGateScript(gate: AuthHealthGate | undefined, healthJsonShellArg: string): string[] { if (gate === undefined) return []; return [ `auth_health_gate_fields=${shellQuote(gate.requiredFields.join(","))}`, `python3 - ${healthJsonShellArg} "$auth_health_gate_fields" <<'PY'`, "import json, sys", "path, fields_text = sys.argv[1:]", "fields = [item for item in fields_text.split(',') if item]", "body = json.load(open(path, encoding='utf-8'))", "auth = body.get('auth') if isinstance(body, dict) else None", "auth = auth if isinstance(auth, dict) else {}", "failed = [field for field in fields if auth.get(field) is not True]", "if failed:", " print('baidu_netdisk_auth_health_gate_failed=' + ','.join(failed))", " raise SystemExit(1)", "print('baidu_netdisk_auth_health_gate=ok')", "PY", ]; } export function upsertEnvFileValues(path: string, values: Record): void { const existing = readFileSync(path, "utf8"); const seen = new Set(); const lines = existing.split(/\n/u).filter((line, index, array) => index < array.length - 1 || line.length > 0).map((line) => { const match = /^([A-Za-z0-9_]+)=/u.exec(line); if (match === null || values[match[1]] === undefined) return line; seen.add(match[1]); return `${match[1]}=${values[match[1]]}`; }); for (const [key, value] of Object.entries(values)) { if (!seen.has(key)) lines.push(`${key}=${value}`); } writeFileSync(path, `${lines.join("\n")}\n`, "utf8"); } export async function pullArtifactFromD601(options: ArtifactRegistryOptions, sourceImage: string): Promise { const localArchive = `/tmp/unidesk-artifact-${safeName(options.serviceId ?? "service")}-${safeName(options.commit ?? "commit")}-${Date.now().toString(36)}.tar.gz`; const remoteArchive = `/tmp/unidesk-artifact-${safeName(options.serviceId ?? "service")}-${safeName(options.commit ?? "commit")}-${Date.now().toString(36)}.tar.gz`; const remoteScript = [ "set -euo pipefail", `image=${shellQuote(sourceImage)}`, `archive=${shellQuote(remoteArchive)}`, "rm -f \"$archive\"", "export DOCKER_CONFIG=$(mktemp -d /tmp/unidesk-artifact-docker-config.XXXXXX)", "trap 'rm -rf \"$DOCKER_CONFIG\"' EXIT", "printf '{}\\n' > \"$DOCKER_CONFIG/config.json\"", "docker pull -q \"$image\" >/dev/null", "docker image inspect \"$image\" --format 'remote_source={{ index .Config.Labels \"unidesk.ai/source-commit\" }} remote_service={{ index .Config.Labels \"unidesk.ai/service-id\" }} remote_dockerfile={{ index .Config.Labels \"unidesk.ai/dockerfile\" }}' >&2", "docker save \"$image\" | gzip -1 > \"$archive\"", "remote_archive_bytes=$(wc -c < \"$archive\" | tr -d '[:space:]')", "remote_archive_sha256=$(sha256sum \"$archive\" | awk '{print $1}')", "printf 'remote_archive=%s\\nremote_archive_bytes=%s\\nremote_archive_sha256=%s\\n' \"$archive\" \"$remote_archive_bytes\" \"$remote_archive_sha256\"", ].join("\n"); const archive = await runRemoteScriptBackground(options, remoteScript, Math.max(options.timeoutMs, 900_000), "docker-save"); if (archive.exitCode !== 0 || archive.timedOut) return archive; const download = downloadRemoteFile(options, remoteArchive, localArchive, Math.max(options.timeoutMs, 900_000)); if (download.exitCode !== 0 || download.timedOut) { runRemoteScript(options, `rm -f ${shellQuote(remoteArchive)}`, 30_000); try { rmSync(localArchive, { force: true }); } catch { // Best-effort cleanup only. } return combineCommandResults(["artifact-registry", "pull-artifact-from-d601"], [archive, download]); } const load = runCommand(["bash", "-lc", `set -euo pipefail; gzip -dc ${shellQuote(localArchive)} | docker load`], repoRoot, { timeoutMs: Math.max(options.timeoutMs, 900_000) }); runRemoteScript(options, `rm -f ${shellQuote(remoteArchive)}`, 30_000); try { rmSync(localArchive, { force: true }); } catch { // Best-effort cleanup only. } return combineCommandResults(["artifact-registry", "pull-artifact-from-d601"], [archive, download, load]); }