feat: add Sub2API image prepull command
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
This commit is contained in:
@@ -393,6 +393,7 @@ export function platformInfraHelp(): unknown {
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts platform-infra sub2api plan [--target G14|D601]",
|
||||
"bun scripts/cli.ts platform-infra sub2api image-prepull [--target G14|D601|PK01] [--dry-run|--confirm] [--wait]",
|
||||
"bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --dry-run",
|
||||
"bun scripts/cli.ts platform-infra sub2api apply [--target G14|D601] --confirm",
|
||||
"bun scripts/cli.ts platform-infra sub2api status [--target G14|D601] [--full|--raw]",
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import type { UniDeskConfig } from "../config";
|
||||
import { capture, compactCapture, parseJsonOutput, shQuote } from "../platform-infra-public-service";
|
||||
import { startJob } from "../jobs";
|
||||
|
||||
import { readSub2ApiConfig } from "./config";
|
||||
import { imageRef, resolveTarget, targetDependencyImages } from "./manifest";
|
||||
import type { ApplyOptions } from "./options";
|
||||
import { boolField } from "./utils";
|
||||
|
||||
function imagePrepullScript(images: string[], confirm: boolean): string {
|
||||
const imageList = images.join("\n");
|
||||
const mode = confirm ? "confirm" : "dry-run";
|
||||
return `
|
||||
set -u
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
mode=${shQuote(mode)}
|
||||
images_file="$tmp/images.txt"
|
||||
cat > "$images_file" <<'EOF_IMAGES'
|
||||
${imageList}
|
||||
EOF_IMAGES
|
||||
docker_bin=""
|
||||
if docker ps >/dev/null 2>&1; then
|
||||
docker_bin="docker"
|
||||
elif sudo docker ps >/dev/null 2>&1; then
|
||||
docker_bin="sudo docker"
|
||||
fi
|
||||
crictl_bin=""
|
||||
if command -v crictl >/dev/null 2>&1; then
|
||||
crictl_bin="crictl"
|
||||
elif sudo crictl images >/dev/null 2>&1; then
|
||||
crictl_bin="sudo crictl"
|
||||
fi
|
||||
runtime="none"
|
||||
if [ -n "$docker_bin" ]; then
|
||||
runtime="docker"
|
||||
elif [ -n "$crictl_bin" ]; then
|
||||
runtime="crictl"
|
||||
fi
|
||||
jsonl="$tmp/images.jsonl"
|
||||
: > "$jsonl"
|
||||
ok=true
|
||||
while IFS= read -r image; do
|
||||
[ -n "$image" ] || continue
|
||||
present_before=false
|
||||
present_after=false
|
||||
pull_rc=0
|
||||
inspect_id=""
|
||||
inspect_digest=""
|
||||
safe_name="$(printf '%s' "$image" | tr '/:@' '___')"
|
||||
pull_stdout="$tmp/$safe_name.pull.out"
|
||||
pull_stderr="$tmp/$safe_name.pull.err"
|
||||
: > "$pull_stdout"
|
||||
: > "$pull_stderr"
|
||||
if [ "$runtime" = "docker" ]; then
|
||||
if $docker_bin image inspect "$image" >/dev/null 2>&1; then present_before=true; fi
|
||||
if [ "$mode" = "confirm" ]; then
|
||||
$docker_bin pull "$image" >"$pull_stdout" 2>"$pull_stderr"
|
||||
pull_rc=$?
|
||||
fi
|
||||
if $docker_bin image inspect "$image" >/dev/null 2>&1; then
|
||||
present_after=true
|
||||
inspect_id="$($docker_bin image inspect "$image" --format '{{.Id}}' 2>/dev/null || true)"
|
||||
inspect_digest="$($docker_bin image inspect "$image" --format '{{join .RepoDigests ","}}' 2>/dev/null || true)"
|
||||
fi
|
||||
elif [ "$runtime" = "crictl" ]; then
|
||||
if $crictl_bin inspecti "$image" >/dev/null 2>&1; then present_before=true; fi
|
||||
if [ "$mode" = "confirm" ]; then
|
||||
$crictl_bin pull "$image" >"$pull_stdout" 2>"$pull_stderr"
|
||||
pull_rc=$?
|
||||
fi
|
||||
if $crictl_bin inspecti "$image" >/dev/null 2>&1; then
|
||||
present_after=true
|
||||
inspect_id="$($crictl_bin inspecti "$image" 2>/dev/null | python3 -c 'import json,sys; data=json.load(sys.stdin); print(data.get("status",{}).get("id") or data.get("id") or "")' 2>/dev/null || true)"
|
||||
fi
|
||||
else
|
||||
pull_rc=1
|
||||
printf '%s\\n' 'neither docker nor crictl is available for image prepull' >"$pull_stderr"
|
||||
fi
|
||||
if [ "$mode" = "confirm" ]; then
|
||||
if [ "$pull_rc" -ne 0 ] || [ "$present_after" != true ]; then ok=false; fi
|
||||
else
|
||||
if [ "$runtime" = "none" ]; then ok=false; fi
|
||||
fi
|
||||
python3 - "$jsonl" "$image" "$runtime" "$present_before" "$present_after" "$pull_rc" "$inspect_id" "$inspect_digest" "$pull_stdout" "$pull_stderr" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
path, image, runtime, before, after, pull_rc, inspect_id, inspect_digest, pull_stdout, pull_stderr = sys.argv[1:]
|
||||
|
||||
def text(file_path: str, limit: int = 4000) -> str:
|
||||
try:
|
||||
return open(file_path, encoding="utf-8", errors="replace").read()[-limit:]
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
|
||||
record = {
|
||||
"image": image,
|
||||
"runtime": runtime,
|
||||
"presentBefore": before == "true",
|
||||
"presentAfter": after == "true",
|
||||
"pull": {
|
||||
"exitCode": int(pull_rc),
|
||||
"stdout": text(pull_stdout),
|
||||
"stderr": text(pull_stderr),
|
||||
},
|
||||
"imageId": inspect_id or None,
|
||||
"repoDigests": [item for item in inspect_digest.split(",") if item],
|
||||
}
|
||||
with open(path, "a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(record, ensure_ascii=False) + "\\n")
|
||||
PY
|
||||
done < "$images_file"
|
||||
python3 - "$jsonl" "$ok" "$mode" "$runtime" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
jsonl, ok, mode, runtime = sys.argv[1:]
|
||||
images = []
|
||||
try:
|
||||
with open(jsonl, encoding="utf-8") as handle:
|
||||
for line in handle:
|
||||
if line.strip():
|
||||
images.append(json.loads(line))
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
payload = {
|
||||
"ok": ok == "true",
|
||||
"mode": mode,
|
||||
"runtime": runtime,
|
||||
"images": images,
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
sys.exit(0 if payload["ok"] else 1)
|
||||
PY
|
||||
`;
|
||||
}
|
||||
|
||||
export async function imagePrepull(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
|
||||
const sub2api = readSub2ApiConfig();
|
||||
const target = resolveTarget(sub2api, options.targetId);
|
||||
const dependencyImages = targetDependencyImages(sub2api, target);
|
||||
const images = Array.from(new Set([imageRef(sub2api, target), dependencyImages.redis]));
|
||||
if (options.confirm && !options.wait) {
|
||||
const job = startJob(
|
||||
`platform_infra_sub2api_image_prepull_${target.id.toLowerCase()}`,
|
||||
["bun", "scripts/cli.ts", "platform-infra", "sub2api", "image-prepull", "--target", target.id, "--confirm", "--wait"],
|
||||
`Pre-pull ${target.id} Sub2API runtime images from YAML-controlled image refs`,
|
||||
);
|
||||
return {
|
||||
ok: true,
|
||||
action: "platform-infra-sub2api-image-prepull",
|
||||
mode: "async-job",
|
||||
target: { id: target.id, route: target.route, namespace: target.namespace },
|
||||
images,
|
||||
job,
|
||||
statusCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
|
||||
next: {
|
||||
status: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`,
|
||||
verify: `bun scripts/cli.ts platform-infra sub2api image-prepull --target ${target.id}`,
|
||||
apply: `bun scripts/cli.ts platform-infra sub2api apply --target ${target.id} --confirm`,
|
||||
},
|
||||
};
|
||||
}
|
||||
const result = await capture(config, target.route, ["sh"], imagePrepullScript(images, options.confirm));
|
||||
const parsed = parseJsonOutput(result.stdout);
|
||||
return {
|
||||
ok: result.exitCode === 0 && boolField(parsed, "ok", false),
|
||||
action: "platform-infra-sub2api-image-prepull",
|
||||
mode: options.confirm ? "confirmed" : "dry-run",
|
||||
target: { id: target.id, route: target.route, namespace: target.namespace },
|
||||
images,
|
||||
remote: parsed ?? compactCapture(result, { full: true }),
|
||||
next: {
|
||||
apply: `bun scripts/cli.ts platform-infra sub2api apply --target ${target.id} --confirm`,
|
||||
status: `bun scripts/cli.ts platform-infra sub2api status --target ${target.id}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { fingerprintSecretValues, parseEnvFile, readEnvSourceFile, readTextFile,
|
||||
import { apply, plan, status } from "./actions";
|
||||
import { defaultSub2ApiTargetId, readSub2ApiConfig, validateOptions } from "./config";
|
||||
import { configPath, platformInfraHelp, serviceName } from "./entry";
|
||||
import { imagePrepull } from "./image-prepull";
|
||||
import { isHostDockerTarget, resolveTarget } from "./manifest";
|
||||
import { validate } from "./policy";
|
||||
|
||||
@@ -88,6 +89,11 @@ export async function runPlatformInfraCommand(config: UniDeskConfig, args: strin
|
||||
const result = await apply(config, options);
|
||||
return options.full || options.raw ? result : renderSub2ApiApply(result);
|
||||
}
|
||||
if (action === "image-prepull") {
|
||||
const options = parseApplyOptions(args.slice(2));
|
||||
const result = await imagePrepull(config, options);
|
||||
return options.full || options.raw ? result : renderSub2ApiImagePrepull(result);
|
||||
}
|
||||
if (action === "status") {
|
||||
const options = parseDisclosureOptions(args.slice(2));
|
||||
const result = await status(config, options);
|
||||
@@ -307,6 +313,64 @@ function renderSub2ApiApply(result: Record<string, unknown>): RenderedCliResult
|
||||
return rendered(result, "platform-infra sub2api apply", lines);
|
||||
}
|
||||
|
||||
function renderSub2ApiImagePrepull(result: Record<string, unknown>): RenderedCliResult {
|
||||
const target = record(result.target);
|
||||
const mode = stringValue(result.mode);
|
||||
const targetId = stringValue(target.id, stringValue(result.target));
|
||||
const status = result.ok === false ? "failed" : "ok";
|
||||
const lines = [
|
||||
"PLATFORM-INFRA SUB2API IMAGE PREPULL",
|
||||
...table(["TARGET", "ROUTE", "NAMESPACE", "MODE", "STATUS"], [[targetId, stringValue(target.route), stringValue(target.namespace), mode, status]]),
|
||||
];
|
||||
if (mode === "async-job") {
|
||||
const job = record(result.job);
|
||||
const next = record(result.next);
|
||||
lines.push(
|
||||
"",
|
||||
"JOB",
|
||||
...table(["ID", "STATUS", "COMMAND"], [[stringValue(job.id), stringValue(job.status, "started"), stringValue(result.statusCommand)]]),
|
||||
"",
|
||||
"IMAGES",
|
||||
...table(["IMAGE"], arrayValues(result.images).map((image) => [stringValue(image)])),
|
||||
"",
|
||||
"NEXT",
|
||||
` status: ${stringValue(next.status, stringValue(result.statusCommand))}`,
|
||||
` verify: ${stringValue(next.verify, `bun scripts/cli.ts platform-infra sub2api image-prepull --target ${targetId}`)}`,
|
||||
` apply: ${stringValue(next.apply, `bun scripts/cli.ts platform-infra sub2api apply --target ${targetId} --confirm`)}`,
|
||||
);
|
||||
return rendered(result, "platform-infra sub2api image-prepull", lines);
|
||||
}
|
||||
const remote = record(result.remote);
|
||||
const imageRows = arrayRecords(remote.images).map((item) => [
|
||||
stringValue(item.image),
|
||||
stringValue(item.runtime),
|
||||
boolText(item.presentBefore),
|
||||
boolText(item.presentAfter),
|
||||
stringValue(record(item.pull).exitCode, "-"),
|
||||
stringValue(item.imageId, "-").slice(0, 24),
|
||||
]);
|
||||
const next = record(result.next);
|
||||
lines.push(
|
||||
"",
|
||||
"IMAGES",
|
||||
...(imageRows.length === 0 ? ["-"] : table(["IMAGE", "RUNTIME", "BEFORE", "AFTER", "PULL", "IMAGE_ID"], imageRows)),
|
||||
"",
|
||||
"CHECKS",
|
||||
...table(["CHECK", "VALUE"], [
|
||||
["ok", boolText(result.ok !== false)],
|
||||
["runtime", stringValue(remote.runtime)],
|
||||
["valuesPrinted", "false"],
|
||||
]),
|
||||
"",
|
||||
"NEXT",
|
||||
` apply: ${stringValue(next.apply, `bun scripts/cli.ts platform-infra sub2api apply --target ${targetId} --confirm`)}`,
|
||||
` status: ${stringValue(next.status, `bun scripts/cli.ts platform-infra sub2api status --target ${targetId}`)}`,
|
||||
` full: bun scripts/cli.ts platform-infra sub2api image-prepull --target ${targetId} ${mode === "confirmed" ? "--confirm --wait" : "--dry-run"} --full`,
|
||||
"Disclosure: default output is bounded; Secret values are not printed.",
|
||||
);
|
||||
return rendered(result, "platform-infra sub2api image-prepull", lines);
|
||||
}
|
||||
|
||||
function renderApplyRemoteSummary(remote: Record<string, unknown>): string[] {
|
||||
const steps = record(remote.steps);
|
||||
const stepRows = Object.entries(steps).map(([name, value]) => {
|
||||
|
||||
Reference in New Issue
Block a user