Files
pikasTech-unidesk/scripts/src/artifact-registry/readonly.ts
T
Lyon edfddd2445 fix: YAML-first 治理 CI/CD target (#919)
* docs: specify cicd yaml target governance

* fix: resolve cicd targets from yaml

---------

Co-authored-by: Codex <codex@noreply.local>
2026-06-26 01:14:38 +08:00

216 lines
9.6 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. readonly module for scripts/src/artifact-registry.ts.
// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets.
// 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 { artifactRegistryOptionsTargetSummary, type ArtifactRegistryCommandRuntime, type ArtifactRegistryOptions, type 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} --target ${options.targetId}`,
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,
"--target",
options.targetId,
"--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,
target: artifactRegistryOptionsTargetSummary(options),
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);
}