Files
pikasTech-unidesk/scripts/src/artifact-registry/install.ts
T
2026-06-25 16:16:25 +00:00

147 lines
5.4 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. install module for scripts/src/artifact-registry.ts.
// Moved mechanically from scripts/src/artifact-registry.ts:2117-2231 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 { ArtifactRegistryOptions, RenderedFile, RuntimeSecretPresence, RuntimeSecretRequirement } from "./types";
import { renderBundle, rootExecPrelude, shellQuote } from "./bundle";
import { commandTail } from "./consumer";
import { runReadonlyStatus } from "./readonly";
import { runRemoteScript } from "./remote";
import { plan } from "./status";
export function remoteWriteFileCommand(item: RenderedFile): string {
const encoded = Buffer.from(item.content, "utf8").toString("base64");
const rootOwned = item.path.startsWith("/etc/");
return [
`target=${shellQuote(item.path)}`,
"tmp=$(mktemp /tmp/unidesk-artifact-registry.XXXXXX)",
"trap 'rm -f \"$tmp\"' EXIT",
`printf %s ${shellQuote(encoded)} | base64 -d > "$tmp"`,
`if [ ${rootOwned ? "1" : "0"} = 1 ]; then`,
" root_exec mkdir -p \"$(dirname \"$target\")\"",
` root_exec install -m ${shellQuote(item.mode)} "$tmp" "$target"`,
"else",
" mkdir -p \"$(dirname \"$target\")\"",
` install -m ${shellQuote(item.mode)} "$tmp" "$target"`,
"fi",
`echo ${shellQuote(`artifact_registry_file_written path=${item.path} sha256=${item.sha256}`)}`,
].join("\n");
}
export function install(options: ArtifactRegistryOptions): Record<string, unknown> {
const bundle = renderBundle(options);
const script = [
"set -euo pipefail",
"command -v docker >/dev/null",
"docker compose version >/dev/null",
"command -v systemctl >/dev/null",
rootExecPrelude(),
`mkdir -p ${shellQuote(bundle.paths.baseDir)} ${shellQuote(bundle.paths.storage)}`,
...bundle.files.map(remoteWriteFileCommand),
"root_exec systemctl daemon-reload",
`root_exec systemctl enable --now ${shellQuote(options.unitName)}`,
"sleep 2",
`curl -fsS http://${options.host}:${options.port}/v2/ >/dev/null`,
].join("\n");
const command = runRemoteScript(options, script, Math.max(options.timeoutMs, 120_000));
const status = runReadonlyStatus(options, true);
return {
ok: command.exitCode === 0 && !command.timedOut && status.ok === true,
dryRun: false,
mutation: true,
providerId: options.providerId,
render: {
paths: bundle.paths,
files: bundle.files.map((item) => ({ path: item.path, mode: item.mode, sha256: item.sha256 })),
},
installCommand: commandTail(command),
health: status,
};
}
export function installDryRun(options: ArtifactRegistryOptions): Record<string, unknown> {
const bundle = renderBundle(options);
return {
ok: true,
dryRun: true,
readonly: true,
mutation: false,
providerId: options.providerId,
plan: plan(options),
render: bundle,
intendedRemoteActions: [
`mkdir -p ${options.baseDir} ${options.storageDir}`,
`write ${bundle.paths.config}`,
`write ${bundle.paths.compose}`,
`write ${bundle.paths.unit}`,
"systemctl daemon-reload",
`systemctl enable --now ${options.unitName}`,
`curl -fsS http://${options.host}:${options.port}/v2/`,
],
note: "Dry run only; no D601 runtime files or services were changed.",
};
}
export function composeLockScript(innerScript: string): string {
const lockDir = rootPath(".state", "locks");
const lockPath = rootPath(".state", "locks", "server-compose.lock");
return [
"set -euo pipefail",
`mkdir -p ${shellQuote(lockDir)}`,
`echo ${shellQuote(`compose_lock_wait ${lockPath}`)}`,
`flock ${shellQuote(lockPath)} bash -lc ${shellQuote(innerScript)}`,
].join("; ");
}
export function findEnvValueLength(raw: string, key: string): number {
for (const line of raw.split(/\r?\n/u)) {
if (!line.trim() || line.trimStart().startsWith("#")) continue;
const index = line.indexOf("=");
if (index <= 0) continue;
if (line.slice(0, index) !== key) continue;
let value = line.slice(index + 1);
if (value.startsWith("\"") && value.endsWith("\"")) {
try {
value = JSON.parse(value) as string;
} catch {
value = value.slice(1, -1);
}
}
return value.length;
}
return 0;
}
export function runtimeSecretPresenceFromEnvText(envText: string, requirements: RuntimeSecretRequirement[]): RuntimeSecretPresence[] {
return requirements.map((requirement) => {
const length = findEnvValueLength(envText, requirement.sourceEnvName);
return {
sourceEnvName: requirement.sourceEnvName,
containerEnvName: requirement.containerEnvName,
present: length > 0,
length,
};
});
}