edfddd2445
* docs: specify cicd yaml target governance * fix: resolve cicd targets from yaml --------- Co-authored-by: Codex <codex@noreply.local>
216 lines
9.6 KiB
TypeScript
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);
|
|
}
|