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> { 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}`, }, }; }