147 lines
5.4 KiB
TypeScript
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,
|
|
};
|
|
});
|
|
}
|