333 lines
14 KiB
TypeScript
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);
|
|
}
|