refactor: split control-plane cli modules
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. readonly module for scripts/src/artifact-registry.ts.
|
||||
|
||||
// Moved mechanically from scripts/src/artifact-registry.ts:1934-2116 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 { ArtifactRegistryCommandRuntime, ArtifactRegistryOptions, ArtifactRegistryReadonlyProbe } from "./types";
|
||||
import { renderBundle } from "./bundle";
|
||||
import { commandTail } from "./consumer";
|
||||
import { annotateRemoteFirstReadonlyResult, annotateRemoteReadonlyResult, controlPlaneMissingResult, parsedCliData, readonlyCommandFailureResult, remoteFrontendHostFromEnv, runRemoteScript, unwrapRemoteArtifactRegistryResult } from "./remote";
|
||||
import { asBool, parseKeyValueOutput, readonlyRemoteCommandShape, registryHealthDecision, statusScript } from "./status";
|
||||
|
||||
export function artifactRegistryReadonlyAutoRemotePlan(
|
||||
action: "status" | "health",
|
||||
options: ArtifactRegistryOptions,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Record<string, unknown> {
|
||||
const remoteHost = remoteFrontendHostFromEnv(env);
|
||||
return {
|
||||
enabled: remoteHost !== null,
|
||||
action,
|
||||
providerId: options.providerId,
|
||||
host: remoteHost,
|
||||
transport: "frontend",
|
||||
command: remoteHost === null ? null : `bun scripts/cli.ts --main-server-ip ${remoteHost} artifact-registry ${action} --provider-id ${options.providerId}`,
|
||||
failureClassification: remoteHost === null ? "control-plane-missing" : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function runReadonlyStatusWithRemoteFallback(options: ArtifactRegistryOptions, healthMode: boolean, runtime: ArtifactRegistryCommandRuntime = {}): Record<string, unknown> {
|
||||
const action = healthMode ? "health" : "status";
|
||||
const remoteHost = remoteFrontendHostFromEnv(runtime.env ?? process.env);
|
||||
const shouldPreferRemote = remoteHost !== null && (
|
||||
Boolean((runtime.env ?? process.env).CODE_QUEUE_SERVICE_ROLE)
|
||||
|| Boolean((runtime.env ?? process.env).CODE_QUEUE_INSTANCE_ID)
|
||||
|| Boolean((runtime.env ?? process.env).KUBERNETES_SERVICE_HOST)
|
||||
|| Boolean((runtime.env ?? process.env).UNIDESK_ARTIFACT_REGISTRY_REMOTE_FIRST)
|
||||
);
|
||||
if (shouldPreferRemote) {
|
||||
const remoteResult = runReadonlyStatusViaRemoteFrontend(options, action, remoteHost, null, runtime);
|
||||
if (remoteResult !== null) return remoteResult;
|
||||
}
|
||||
const localResult = runReadonlyStatus(options, healthMode, runtime);
|
||||
if (localResult.failureClassification !== "local-docker-required") return localResult;
|
||||
if (remoteHost === null) return controlPlaneMissingResult(options, action, localResult, null, null, remoteHost);
|
||||
const remoteResult = runReadonlyStatusViaRemoteFrontend(options, action, remoteHost, localResult, runtime);
|
||||
if (remoteResult !== null) return remoteResult;
|
||||
return controlPlaneMissingResult(options, action, localResult, null, null, remoteHost);
|
||||
}
|
||||
|
||||
export function runReadonlyStatusViaRemoteFrontend(
|
||||
options: ArtifactRegistryOptions,
|
||||
action: "status" | "health",
|
||||
remoteHost: string,
|
||||
localResult: Record<string, unknown> | null,
|
||||
runtime: ArtifactRegistryCommandRuntime,
|
||||
): Record<string, unknown> | null {
|
||||
const remoteCommandArgs = [
|
||||
process.execPath,
|
||||
rootPath("scripts", "cli.ts"),
|
||||
"--main-server-ip",
|
||||
remoteHost,
|
||||
"artifact-registry",
|
||||
action,
|
||||
"--provider-id",
|
||||
options.providerId,
|
||||
"--timeout-ms",
|
||||
String(options.timeoutMs),
|
||||
];
|
||||
const remoteTimeoutMs = Math.max(options.timeoutMs + 15_000, 45_000);
|
||||
const remoteCommand = runtime.runCliForTest === undefined
|
||||
? runCommand(remoteCommandArgs, repoRoot, { timeoutMs: remoteTimeoutMs })
|
||||
: runtime.runCliForTest(remoteCommandArgs, remoteTimeoutMs);
|
||||
const parsed = parsedCliData(remoteCommand.stdout);
|
||||
const remoteResult = parsed === null ? null : unwrapRemoteArtifactRegistryResult(parsed);
|
||||
if (remoteCommand.exitCode === 0 && !remoteCommand.timedOut && remoteResult !== null) {
|
||||
return localResult === null
|
||||
? annotateRemoteFirstReadonlyResult(remoteResult, remoteHost, remoteCommand)
|
||||
: annotateRemoteReadonlyResult(remoteResult, localResult, remoteHost, remoteCommand);
|
||||
}
|
||||
if (localResult !== null) return controlPlaneMissingResult(options, action, localResult, remoteResult, remoteCommand, remoteHost);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function statusFromValues(options: ArtifactRegistryOptions, values: Record<string, string>, command: CommandResult, healthMode: boolean): Record<string, unknown> {
|
||||
const commandOk = command.exitCode === 0 && !command.timedOut;
|
||||
const bundle = renderBundle(options);
|
||||
const hashes = Object.fromEntries(bundle.files.map((item) => [item.path, item.sha256]));
|
||||
const checks = {
|
||||
systemctlAvailable: asBool(values.systemctl_available),
|
||||
dockerAvailable: asBool(values.docker_available),
|
||||
curlAvailable: asBool(values.curl_available),
|
||||
unitExists: asBool(values.unit_exists),
|
||||
unitActive: values.unit_active === "active",
|
||||
unitEnabled: values.unit_enabled === "enabled",
|
||||
composeExists: asBool(values.compose_exists),
|
||||
configExists: asBool(values.config_exists),
|
||||
storageExists: asBool(values.storage_exists),
|
||||
containerRunning: asBool(values.container_running),
|
||||
loopbackOnly: asBool(values.loopback_only),
|
||||
v2Ok: values.v2_http_code === "200",
|
||||
imageMatches: asBool(values.image_matches),
|
||||
configHashMatches: asBool(values.config_hash_matches),
|
||||
composeHashMatches: asBool(values.compose_hash_matches),
|
||||
unitHashMatches: asBool(values.unit_hash_matches),
|
||||
};
|
||||
const installed = checks.unitExists || checks.composeExists || checks.configExists || checks.storageExists || checks.containerRunning;
|
||||
const healthy = commandOk
|
||||
&& checks.systemctlAvailable
|
||||
&& checks.dockerAvailable
|
||||
&& checks.unitExists
|
||||
&& checks.unitActive
|
||||
&& checks.composeExists
|
||||
&& checks.configExists
|
||||
&& checks.storageExists
|
||||
&& checks.containerRunning
|
||||
&& checks.loopbackOnly
|
||||
&& checks.v2Ok
|
||||
&& checks.imageMatches
|
||||
&& checks.configHashMatches
|
||||
&& checks.composeHashMatches
|
||||
&& checks.unitHashMatches;
|
||||
const decision = registryHealthDecision(checks, commandOk);
|
||||
return {
|
||||
ok: healthMode ? healthy : commandOk,
|
||||
readonly: true,
|
||||
installed,
|
||||
healthy,
|
||||
...decision,
|
||||
remoteCommandShape: readonlyRemoteCommandShape(healthMode ? "health" : "status", options),
|
||||
checks,
|
||||
observed: {
|
||||
unit: { path: bundle.paths.unit, active: values.unit_active, enabled: values.unit_enabled },
|
||||
compose: { path: bundle.paths.compose, sha256: values.compose_hash ?? null },
|
||||
config: { path: bundle.paths.config, sha256: values.config_hash ?? null },
|
||||
storage: { path: bundle.paths.storage },
|
||||
container: {
|
||||
name: options.containerName,
|
||||
running: values.container_running,
|
||||
status: values.container_status,
|
||||
image: values.container_image,
|
||||
restartPolicy: values.container_restart_policy,
|
||||
},
|
||||
listener: {
|
||||
count: Number(values.listener_count ?? 0),
|
||||
badCount: Number(values.bad_listener_count ?? 0),
|
||||
loopbackOnly: checks.loopbackOnly,
|
||||
},
|
||||
registryApi: { v2HttpCode: values.v2_http_code },
|
||||
},
|
||||
expected: {
|
||||
unitHash: hashes[bundle.paths.unit] ?? "",
|
||||
composeHash: hashes[bundle.paths.compose] ?? "",
|
||||
configHash: hashes[bundle.paths.config] ?? "",
|
||||
image: options.image,
|
||||
endpoint: `http://${options.host}:${options.port}`,
|
||||
},
|
||||
command: commandTail(command),
|
||||
};
|
||||
}
|
||||
|
||||
export function runReadonlyStatus(options: ArtifactRegistryOptions, healthMode: boolean, runtime: ArtifactRegistryCommandRuntime = {}): Record<string, unknown> {
|
||||
const bundle = renderBundle(options);
|
||||
const script = statusScript(options, bundle);
|
||||
const result = runRemoteScript(options, script, options.timeoutMs, runtime);
|
||||
if (result.exitCode !== 0 || result.timedOut) {
|
||||
return readonlyCommandFailureResult(options, result, healthMode ? "health" : "status");
|
||||
}
|
||||
return statusFromValues(options, parseKeyValueOutput(result.stdout), result, healthMode);
|
||||
}
|
||||
|
||||
export function buildArtifactRegistryReadonlyProbe(action: "status" | "health", options: ArtifactRegistryOptions): ArtifactRegistryReadonlyProbe {
|
||||
const bundle = renderBundle(options);
|
||||
const healthMode = action === "health";
|
||||
return {
|
||||
action,
|
||||
providerId: options.providerId,
|
||||
script: statusScript(options, bundle),
|
||||
timeoutMs: options.timeoutMs,
|
||||
healthMode,
|
||||
options,
|
||||
remoteCommandShape: readonlyRemoteCommandShape(action, options),
|
||||
};
|
||||
}
|
||||
|
||||
export function artifactRegistryReadonlyResultFromCommand(
|
||||
probe: ArtifactRegistryReadonlyProbe,
|
||||
command: CommandResult,
|
||||
): Record<string, unknown> {
|
||||
if (command.exitCode !== 0 || command.timedOut) {
|
||||
return readonlyCommandFailureResult(probe.options, command, probe.action);
|
||||
}
|
||||
return statusFromValues(probe.options, parseKeyValueOutput(command.stdout), command, probe.healthMode);
|
||||
}
|
||||
Reference in New Issue
Block a user