Files
pikasTech-unidesk/scripts/src/hwlab-g14/images.ts
T
2026-06-25 16:16:25 +00:00

333 lines
14 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. images module for scripts/src/hwlab-g14.ts.
// Moved mechanically from scripts/src/hwlab-g14.ts:8075-8386 for #903.
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { createHash, randomBytes } from "node:crypto";
import { repoRoot, rootPath, type Config } from "../config";
import { runCommand } from "../command";
import { cancelJob, listJobs, readJob, startJob } from "../jobs";
import { hwlabRequiredNoProxyEntries, hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "../hwlab-node-lanes";
import type { CommandJsonResult, G14ControlPlaneOptions, G14GitMirrorOptions, G14ToolsImageOptions, G14UpstreamImageOptions } from "./types";
import { statusText } from "./pr-monitor";
import { cliJson, isCommandSuccess, record, shellQuote } from "./remote";
import { G14_CI_TOOLS_IMAGE_REPO, G14_PROVIDER, GIT_MIRROR_NAMESPACE, V02_REGISTRY_PREFIX, V02_WORKSPACE } from "./types";
export function startAsyncHwlabG14Job(name: string, command: string[], note: string): Record<string, unknown> {
const job = startJob(name, command, note);
const statusCommand = `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`;
return {
ok: true,
mode: "async-job",
job,
statusCommand,
tailCommand: `tail -f ${job.stdoutFile}`,
next: {
status: statusCommand,
tail: `tail -f ${job.stdoutFile}`,
},
};
}
export function startControlPlaneTriggerJob(options: G14ControlPlaneOptions): Record<string, unknown> {
const command = [
"bun",
"scripts/cli.ts",
"hwlab",
"g14",
"control-plane",
"trigger-current",
"--lane",
options.lane,
"--confirm",
"--timeout-seconds",
String(options.timeoutSeconds),
"--wait",
];
if (options.rerun) command.splice(command.length - 1, 0, "--rerun");
return {
command: `hwlab g14 control-plane trigger-current --lane ${options.lane}`,
lane: options.lane,
reason: "confirmed trigger can spend tens of seconds syncing git mirror and creating PipelineRun; default is fire-and-forget to avoid silent blocking",
waitCommand: command.join(" "),
...startAsyncHwlabG14Job(
`hwlab_g14_${options.lane}_trigger_current`,
command,
`Trigger HWLAB ${options.lane} current commit PipelineRun with git mirror pre-sync through G14 control-plane`,
),
};
}
export function startGitMirrorJob(options: G14GitMirrorOptions): Record<string, unknown> {
const command = [
"bun",
"scripts/cli.ts",
"hwlab",
"g14",
"git-mirror",
options.action,
"--lane",
options.lane,
"--confirm",
"--timeout-seconds",
String(options.timeoutSeconds),
"--wait",
];
return {
command: `hwlab g14 git-mirror ${options.action} --lane ${options.lane}`,
lane: options.lane,
namespace: GIT_MIRROR_NAMESPACE,
reason: "manual git mirror sync/flush waits for a Kubernetes Job; default is fire-and-forget to keep CLI output immediately visible",
waitCommand: command.join(" "),
...startAsyncHwlabG14Job(
`hwlab_g14_git_mirror_${options.action}`,
command,
`Run HWLAB devops-infra git mirror ${options.action} through a bounded manual Kubernetes Job`,
),
};
}
export function g14HostScript(script: string, timeoutMs = 120_000): CommandJsonResult {
return cliJson(["ssh", G14_PROVIDER, "sh", "--", script], timeoutMs);
}
export function g14CiToolsImage(tag: string): string {
return `${G14_CI_TOOLS_IMAGE_REPO}:${tag}`;
}
export function runG14ToolsImageStatus(options: G14ToolsImageOptions): Record<string, unknown> {
const image = g14CiToolsImage(options.tag);
const script = [
"set -u",
`image=${shellQuote(image)}`,
"exists=false",
"image_id=",
"created=",
"size=",
"node_version=",
"npm_version=",
"bun_version=",
"git_version=",
"docker_version=",
"python_version=",
"registry_tags=",
"if docker image inspect \"$image\" >/tmp/hwlab-tools-image-inspect.json 2>/tmp/hwlab-tools-image-inspect.err; then",
" exists=true",
" image_id=$(docker image inspect \"$image\" --format '{{.Id}}' 2>/dev/null || true)",
" created=$(docker image inspect \"$image\" --format '{{.Created}}' 2>/dev/null || true)",
" size=$(docker image inspect \"$image\" --format '{{.Size}}' 2>/dev/null || true)",
" node_version=$(docker run --rm \"$image\" node --version 2>/dev/null || true)",
" npm_version=$(docker run --rm \"$image\" npm --version 2>/dev/null || true)",
" bun_version=$(docker run --rm \"$image\" bun --version 2>/dev/null || true)",
" git_version=$(docker run --rm \"$image\" git --version 2>/dev/null || true)",
" docker_version=$(docker run --rm \"$image\" docker --version 2>/dev/null || true)",
" python_version=$(docker run --rm \"$image\" python3 --version 2>/dev/null || true)",
"fi",
`registry_tags=$(curl -fsS --max-time 10 http://127.0.0.1:5000/v2/hwlab/hwlab-ci-node-tools/tags/list 2>/dev/null || true)`,
"export image exists image_id created size node_version npm_version bun_version git_version docker_version python_version registry_tags",
"node - <<'NODE'",
"const fs = require('node:fs');",
"const env = process.env;",
"const payload = {",
" image: env.image,",
" exists: env.exists === 'true',",
" imageId: env.image_id || null,",
" created: env.created || null,",
" sizeBytes: env.size ? Number(env.size) : null,",
" tools: {",
" node: env.node_version || null,",
" npm: env.npm_version || null,",
" bun: env.bun_version || null,",
" git: env.git_version || null,",
" docker: env.docker_version || null,",
" python: env.python_version || null",
" },",
" registryTagsRaw: env.registry_tags || null",
"};",
"console.log(JSON.stringify(payload, null, 2));",
"NODE",
].join("\n");
const result = g14HostScript(script, 180_000);
let parsedStatus: unknown = null;
const text = statusText(result);
if (text.length > 0) {
try {
parsedStatus = JSON.parse(text) as unknown;
} catch {
parsedStatus = null;
}
}
return {
ok: isCommandSuccess(result) && record(parsedStatus).exists === true,
command: "hwlab g14 tools-image status --name ci-node-tools",
name: options.name,
image,
status: parsedStatus ?? text,
result,
};
}
export function runG14ToolsImageBuild(options: G14ToolsImageOptions): Record<string, unknown> {
const image = g14CiToolsImage(options.tag);
const script = [
"set -eu",
`cd ${shellQuote(V02_WORKSPACE)}`,
"git fetch origin v0.2 --prune",
"git checkout v0.2 >/dev/null 2>&1 || true",
"git merge --ff-only origin/v0.2",
`test -f ${shellQuote(options.dockerfile)}`,
`image=${shellQuote(image)}`,
`dockerfile=${shellQuote(options.dockerfile)}`,
"echo \"{\\\"phase\\\":\\\"preflight\\\",\\\"image\\\":\\\"$image\\\",\\\"dockerfile\\\":\\\"$dockerfile\\\",\\\"commit\\\":\\\"$(git rev-parse HEAD)\\\"}\"",
"export HTTP_PROXY=http://127.0.0.1:10808 HTTPS_PROXY=http://127.0.0.1:10808 http_proxy=http://127.0.0.1:10808 https_proxy=http://127.0.0.1:10808",
"export NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal,74.48.78.17,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,10.42.0.0/16,10.43.0.0/16,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,127.0.0.1:5000,localhost:5000",
"export no_proxy=$NO_PROXY",
"docker build --pull --build-arg HTTP_PROXY --build-arg HTTPS_PROXY --build-arg http_proxy --build-arg https_proxy --build-arg NO_PROXY --build-arg no_proxy -f \"$dockerfile\" -t \"$image\" .",
"docker run --rm \"$image\" sh -lc 'node --version && npm --version && bun --version && git --version && python3 --version && docker --version && ssh -V'",
"docker push \"$image\"",
"digest=$(docker image inspect \"$image\" --format '{{index .RepoDigests 0}}' 2>/dev/null || true)",
"echo \"{\\\"phase\\\":\\\"published\\\",\\\"image\\\":\\\"$image\\\",\\\"digest\\\":\\\"$digest\\\"}\"",
].join("\n");
if (options.dryRun) {
return {
ok: true,
command: "hwlab g14 tools-image build --name ci-node-tools",
mode: "dry-run",
image,
workspace: V02_WORKSPACE,
dockerfile: options.dockerfile,
buildScriptPreview: script.split(/\n/u),
next: { build: `bun scripts/cli.ts hwlab g14 tools-image build --name ci-node-tools --tag ${options.tag} --confirm` },
};
}
const result = g14HostScript(script, options.timeoutSeconds * 1000);
return {
ok: isCommandSuccess(result),
command: "hwlab g14 tools-image build --name ci-node-tools",
mode: "confirmed-build",
image,
workspace: V02_WORKSPACE,
dockerfile: options.dockerfile,
result,
status: runG14ToolsImageStatus({ ...options, action: "status", dryRun: true, confirm: false }),
};
}
export function runG14ToolsImage(options: G14ToolsImageOptions): Record<string, unknown> {
if (options.action === "status") return runG14ToolsImageStatus(options);
return runG14ToolsImageBuild(options);
}
export function g14UpstreamImageTarget(options: G14UpstreamImageOptions): string {
return `${V02_REGISTRY_PREFIX}/${options.name}:${options.tag}`;
}
export function g14UpstreamImageSource(options: G14UpstreamImageOptions): string {
if (options.name === "openfga") return `docker.io/openfga/openfga:${options.tag}`;
return `${options.name}:${options.tag}`;
}
export function runG14UpstreamImageStatus(options: G14UpstreamImageOptions): Record<string, unknown> {
const sourceImage = g14UpstreamImageSource(options);
const targetImage = g14UpstreamImageTarget(options);
const script = [
"set +e",
`source_image=${shellQuote(sourceImage)}`,
`target_image=${shellQuote(targetImage)}`,
`repo_path=${shellQuote(`hwlab/${options.name}`)}`,
`tag=${shellQuote(options.tag)}`,
"local_id=$(docker image inspect \"$target_image\" --format '{{.Id}}' 2>/dev/null || true)",
"local_created=$(docker image inspect \"$target_image\" --format '{{.Created}}' 2>/dev/null || true)",
"registry_tags=$(curl -fsS --max-time 10 \"http://127.0.0.1:5000/v2/$repo_path/tags/list\" 2>/dev/null || true)",
"registry_has_tag=false",
"if printf '%s' \"$registry_tags\" | grep -F '\"'$tag'\"' >/dev/null 2>&1; then registry_has_tag=true; fi",
"export source_image target_image local_id local_created registry_tags registry_has_tag",
"node <<'NODE'",
"const env = process.env;",
"console.log(JSON.stringify({",
" sourceImage: env.source_image,",
" targetImage: env.target_image,",
" localExists: Boolean(env.local_id),",
" localImageId: env.local_id || null,",
" localCreated: env.local_created || null,",
" registryHasTag: env.registry_has_tag === 'true',",
" registryTagsRaw: env.registry_tags || null",
"}, null, 2));",
"NODE",
].join("\n");
const result = g14HostScript(script, 120_000);
let parsedStatus: unknown = null;
const text = statusText(result);
if (text.length > 0) {
try {
parsedStatus = JSON.parse(text) as unknown;
} catch {
parsedStatus = null;
}
}
return {
ok: isCommandSuccess(result) && record(parsedStatus).registryHasTag === true,
command: "hwlab g14 upstream-image status --name openfga",
name: options.name,
tag: options.tag,
sourceImage,
targetImage,
status: parsedStatus ?? text,
result,
};
}
export function runG14UpstreamImageEnsure(options: G14UpstreamImageOptions): Record<string, unknown> {
const sourceImage = g14UpstreamImageSource(options);
const targetImage = g14UpstreamImageTarget(options);
if (options.dryRun) {
return {
ok: true,
command: "hwlab g14 upstream-image ensure --name openfga",
mode: "dry-run",
name: options.name,
tag: options.tag,
sourceImage,
targetImage,
mutation: false,
next: { confirm: `bun scripts/cli.ts hwlab g14 upstream-image ensure --name ${options.name} --tag ${options.tag} --confirm` },
};
}
const script = [
"set -eu",
`source_image=${shellQuote(sourceImage)}`,
`target_image=${shellQuote(targetImage)}`,
"export HTTP_PROXY=http://127.0.0.1:10808 HTTPS_PROXY=http://127.0.0.1:10808 http_proxy=http://127.0.0.1:10808 https_proxy=http://127.0.0.1:10808",
"export NO_PROXY=localhost,127.0.0.1,::1,host.docker.internal,74.48.78.17,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,10.42.0.0/16,10.43.0.0/16,.svc,.svc.cluster.local,.cluster.local,kubernetes,kubernetes.default,kubernetes.default.svc,127.0.0.1:5000,localhost:5000",
"export no_proxy=$NO_PROXY",
"echo \"{\\\"phase\\\":\\\"pull\\\",\\\"sourceImage\\\":\\\"$source_image\\\"}\"",
"docker pull \"$source_image\"",
"docker tag \"$source_image\" \"$target_image\"",
"echo \"{\\\"phase\\\":\\\"push\\\",\\\"targetImage\\\":\\\"$target_image\\\"}\"",
"docker push \"$target_image\"",
"digest=$(docker image inspect \"$target_image\" --format '{{index .RepoDigests 0}}' 2>/dev/null || true)",
"echo \"{\\\"phase\\\":\\\"published\\\",\\\"targetImage\\\":\\\"$target_image\\\",\\\"digest\\\":\\\"$digest\\\"}\"",
].join("\n");
const result = g14HostScript(script, options.timeoutSeconds * 1000);
const status = runG14UpstreamImageStatus(options);
return {
ok: isCommandSuccess(result) && status.ok === true,
command: "hwlab g14 upstream-image ensure --name openfga",
mode: "confirmed-ensure",
name: options.name,
tag: options.tag,
sourceImage,
targetImage,
mutation: true,
result,
status,
};
}
export function runG14UpstreamImage(options: G14UpstreamImageOptions): Record<string, unknown> {
if (options.action === "status") return runG14UpstreamImageStatus(options);
return runG14UpstreamImageEnsure(options);
}