diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index 337df7aa..1b41baa9 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -258,6 +258,7 @@ lanes: - hwlab-agent-skills buildkit: sidecarImage: 127.0.0.1:5000/hwlab/buildkit:rootless + sourceImage: docker.io/moby/buildkit:rootless stepEnv: HOME: /tekton/home XDG_CONFIG_HOME: /tekton/home/.config @@ -618,6 +619,7 @@ lanes: - hwlab-agent-skills buildkit: sidecarImage: 127.0.0.1:5000/hwlab/buildkit:rootless + sourceImage: docker.io/moby/buildkit:rootless stepEnv: HOME: /tekton/home XDG_CONFIG_HOME: /tekton/home/.config diff --git a/scripts/src/hwlab-node-lanes.ts b/scripts/src/hwlab-node-lanes.ts index ca6a2bd6..da903bd8 100644 --- a/scripts/src/hwlab-node-lanes.ts +++ b/scripts/src/hwlab-node-lanes.ts @@ -190,6 +190,7 @@ export interface HwlabRuntimeWebProbeProjectManagementSpec { export interface HwlabRuntimeBuildkitSpec { readonly sidecarImage: string; + readonly sourceImage: string; } export interface HwlabRuntimeObservabilitySpec { @@ -650,6 +651,7 @@ function buildkitConfig(value: unknown, path: string): HwlabRuntimeBuildkitSpec const raw = asRecord(value, path); return { sidecarImage: stringField(raw, "sidecarImage", path), + sourceImage: stringField(raw, "sourceImage", path), }; } diff --git a/scripts/src/hwlab-node/control-actions.ts b/scripts/src/hwlab-node/control-actions.ts index 00f0c002..6c11e90f 100644 --- a/scripts/src/hwlab-node/control-actions.ts +++ b/scripts/src/hwlab-node/control-actions.ts @@ -174,7 +174,7 @@ export function nodeRuntimeBaseImageCommand(scoped: ReturnType `${functionName} ${shellQuote(image.id)} ${shellQuote(image.target)} ${shellQuote(image.source)}`) + .join("\n"); +} + +function parseNodeRuntimeImageRows(text: string): Record[] { + return text.split(/\r?\n/u).flatMap((line) => { + if (!line.startsWith("imageJson\t")) return []; + try { + return [JSON.parse(line.slice("imageJson\t".length)) as Record]; + } catch { + return []; + } + }); +} + export function nodeRuntimeBaseImageStatus(spec: HwlabRuntimeLaneSpec, timeoutSeconds: number): Record { - const source = spec.baseImageSource ?? ""; const script = [ "set -eu", - `target=${shellQuote(spec.baseImage)}`, - `source=${shellQuote(source)}`, - "repo_tag=${target#*/}", - "repo=${repo_tag%:*}", - "tag=${repo_tag##*:}", - "if [ \"$repo\" = \"$repo_tag\" ]; then tag=latest; fi", - "registry_url=\"http://127.0.0.1:5000/v2/$repo/tags/list\"", - "registry_present=false", - "if curl -fsS \"$registry_url\" 2>/dev/null | grep -q '\"'\"$tag\"'\"'; then registry_present=true; fi", - "target_present=false", - "if docker image inspect \"$target\" >/dev/null 2>&1; then target_present=true; fi", - "source_present=false", - "if [ -n \"$source\" ] && docker image inspect \"$source\" >/dev/null 2>&1; then source_present=true; fi", - "printf 'target\\t%s\\n' \"$target\"", - "printf 'source\\t%s\\n' \"$source\"", - "printf 'sourceConfigured\\t%s\\n' \"$([ -n \"$source\" ] && printf true || printf false)\"", - "printf 'registryUrl\\t%s\\n' \"$registry_url\"", - "printf 'registryTagPresent\\t%s\\n' \"$registry_present\"", - "printf 'targetImagePresent\\t%s\\n' \"$target_present\"", - "printf 'sourceImagePresent\\t%s\\n' \"$source_present\"", + "check_image() {", + " id=\"$1\"; target=\"$2\"; source=\"$3\"", + " repo_tag=${target#*/}", + " repo=${repo_tag%:*}", + " tag=${repo_tag##*:}", + " if [ \"$repo\" = \"$repo_tag\" ]; then tag=latest; fi", + " registry_url=\"http://127.0.0.1:5000/v2/$repo/tags/list\"", + " registry_present=false", + " if curl -fsS \"$registry_url\" 2>/dev/null | grep -q '\"'\"$tag\"'\"'; then registry_present=true; fi", + " target_present=false", + " if docker image inspect \"$target\" >/dev/null 2>&1; then target_present=true; fi", + " source_present=false", + " if [ -n \"$source\" ] && docker image inspect \"$source\" >/dev/null 2>&1; then source_present=true; fi", + " source_configured=false", + " if [ -n \"$source\" ]; then source_configured=true; fi", + " python3 - \"$id\" \"$target\" \"$source\" \"$source_configured\" \"$registry_url\" \"$registry_present\" \"$target_present\" \"$source_present\" <<'PY'", + "import json, sys", + "keys=['id','target','source','sourceConfigured','registryUrl','registryTagPresent','targetImagePresent','sourceImagePresent']", + "data=dict(zip(keys, sys.argv[1:]))", + "for key in ['sourceConfigured','registryTagPresent','targetImagePresent','sourceImagePresent']:", + " data[key] = data[key] == 'true'", + "print('imageJson\\t' + json.dumps(data, separators=(',', ':')))", + "PY", + "}", + nodeRuntimeImageCalls(spec, "check_image"), ].join("\n"); const result = runNodeHostScript(spec, script, Math.min(timeoutSeconds, 300)); - const fields = keyValueLinesFromText(statusText(result)); - const sourceConfigured = fields.sourceConfigured === "true"; - const registryTagPresent = fields.registryTagPresent === "true"; + const imageRows = parseNodeRuntimeImageRows(statusText(result)); + const base = imageRows.find((image) => image.id === "base") ?? {}; return { - ok: isCommandSuccess(result) && sourceConfigured && registryTagPresent, - target: fields.target ?? spec.baseImage, - source: fields.source || null, - sourceConfigured, - registryUrl: fields.registryUrl ?? null, - registryTagPresent, - targetImagePresent: fields.targetImagePresent === "true", - sourceImagePresent: fields.sourceImagePresent === "true", + ok: isCommandSuccess(result) && imageRows.length === nodeRuntimeImageDependencies(spec).length && imageRows.every((image) => image.sourceConfigured === true && image.registryTagPresent === true), + target: base.target ?? spec.baseImage, + source: base.source || null, + sourceConfigured: base.sourceConfigured === true, + registryUrl: base.registryUrl ?? null, + registryTagPresent: base.registryTagPresent === true, + targetImagePresent: base.targetImagePresent === true, + sourceImagePresent: base.sourceImagePresent === true, + images: imageRows, result: compactRuntimeCommand(result), }; } @@ -851,68 +888,80 @@ export function ensureNodeBaseImage(spec: HwlabRuntimeLaneSpec, dryRun: boolean, if (spec.baseImageSource === undefined) return null; const script = [ "set -eu", - `target=${shellQuote(spec.baseImage)}`, - `source=${shellQuote(spec.baseImageSource)}`, `dry_run=${shellQuote(dryRun ? "true" : "false")}`, `pull_retries=${shellQuote(String(Math.max(1, spec.downloadProfile.docker.pullRetries)))}`, - "repo_tag=${target#*/}", - "repo=${repo_tag%:*}", - "tag=${repo_tag##*:}", - "if [ \"$repo\" = \"$repo_tag\" ]; then tag=latest; fi", - "registry_url=\"http://127.0.0.1:5000/v2/$repo/tags/list\"", - "present=false", - "if curl -fsS \"$registry_url\" 2>/dev/null | grep -q '\"'\"$tag\"'\"'; then present=true; fi", - "action=none", - "if [ \"$present\" = false ]; then", - " action=seed", - " if [ \"$dry_run\" = false ]; then", - " source_present_before=true", - " if ! docker image inspect \"$source\" >/dev/null 2>&1; then", - " source_present_before=false", - " attempt=1", - " pulled_source=false", - " while [ \"$attempt\" -le \"$pull_retries\" ]; do", - " pull_attempts=$attempt", - " if docker pull \"$source\" >/tmp/hwlab-node-base-image-pull.out 2>&1; then pulled_source=true; break; fi", - " attempt=$((attempt + 1))", - " sleep 2", - " done", - " if [ \"$pulled_source\" != true ]; then cat /tmp/hwlab-node-base-image-pull.out >&2 2>/dev/null || true; fi", + "failed=false", + "ensure_image() {", + " id=\"$1\"; target=\"$2\"; source=\"$3\"", + " repo_tag=${target#*/}", + " repo=${repo_tag%:*}", + " tag=${repo_tag##*:}", + " if [ \"$repo\" = \"$repo_tag\" ]; then tag=latest; fi", + " registry_url=\"http://127.0.0.1:5000/v2/$repo/tags/list\"", + " present=false", + " if curl -fsS \"$registry_url\" 2>/dev/null | grep -q '\"'\"$tag\"'\"'; then present=true; fi", + " action=none", + " source_present_before=unknown", + " pulled_source=false", + " pull_attempts=0", + " if [ \"$present\" = false ]; then", + " action=seed", + " if [ \"$dry_run\" = false ]; then", + " source_present_before=true", + " if ! docker image inspect \"$source\" >/dev/null 2>&1; then", + " source_present_before=false", + " attempt=1", + " while [ \"$attempt\" -le \"$pull_retries\" ]; do", + " pull_attempts=$attempt", + " if docker pull \"$source\" >/tmp/hwlab-node-runtime-image-$id-pull.out 2>&1; then pulled_source=true; break; fi", + " attempt=$((attempt + 1))", + " sleep 2", + " done", + " if [ \"$pulled_source\" != true ]; then cat /tmp/hwlab-node-runtime-image-$id-pull.out >&2 2>/dev/null || true; failed=true; fi", + " fi", + " if [ \"$failed\" != true ]; then", + " if docker image inspect \"$source\" >/dev/null 2>&1; then", + " docker tag \"$source\" \"$target\"", + " docker push \"$target\" >/tmp/hwlab-node-runtime-image-$id-push.out 2>&1 || failed=true", + " else", + " failed=true", + " fi", + " fi", " fi", - " docker image inspect \"$source\" >/dev/null", - " docker tag \"$source\" \"$target\"", - " docker push \"$target\" >/tmp/hwlab-node-base-image-push.out", " fi", - "fi", - "after=false", - "if curl -fsS \"$registry_url\" 2>/dev/null | grep -q '\"'\"$tag\"'\"'; then after=true; fi", - "printf 'target\\t%s\\n' \"$target\"", - "printf 'source\\t%s\\n' \"$source\"", - "printf 'dryRun\\t%s\\n' \"$dry_run\"", - "printf 'presentBefore\\t%s\\n' \"$present\"", - "printf 'presentAfter\\t%s\\n' \"$after\"", - "printf 'action\\t%s\\n' \"$action\"", - "printf 'pullRetries\\t%s\\n' \"$pull_retries\"", - "printf 'sourcePresentBefore\\t%s\\n' \"${source_present_before:-unknown}\"", - "printf 'pulledSource\\t%s\\n' \"${pulled_source:-false}\"", - "printf 'pullAttempts\\t%s\\n' \"${pull_attempts:-0}\"", - "if [ \"$dry_run\" = true ] || [ \"$after\" = true ]; then exit 0; fi", - "exit 1", + " after=false", + " if curl -fsS \"$registry_url\" 2>/dev/null | grep -q '\"'\"$tag\"'\"'; then after=true; fi", + " python3 - \"$id\" \"$target\" \"$source\" \"$dry_run\" \"$present\" \"$after\" \"$action\" \"$pull_retries\" \"$source_present_before\" \"$pulled_source\" \"$pull_attempts\" <<'PY'", + "import json, sys", + "keys=['id','target','source','dryRun','presentBefore','presentAfter','action','pullRetries','sourcePresentBefore','pulledSource','pullAttempts']", + "data=dict(zip(keys, sys.argv[1:]))", + "for key in ['dryRun','presentBefore','presentAfter','pulledSource']:", + " data[key] = data[key] == 'true'", + "for key in ['pullRetries','pullAttempts']:", + " data[key] = int(data[key]) if str(data[key]).isdigit() else None", + "print('imageJson\\t' + json.dumps(data, separators=(',', ':')))", + "PY", + " if [ \"$dry_run\" = false ] && [ \"$after\" != true ]; then failed=true; fi", + "}", + nodeRuntimeImageCalls(spec, "ensure_image"), + "if [ \"$failed\" = true ]; then exit 1; fi", ].join("\n"); const result = runNodeHostScript(spec, script, Math.min(timeoutSeconds, 300)); - const fields = keyValueLinesFromText(statusText(result)); + const imageRows = parseNodeRuntimeImageRows(statusText(result)); + const base = imageRows.find((image) => image.id === "base") ?? {}; return { ok: isCommandSuccess(result), dryRun, - target: fields.target ?? spec.baseImage, - source: fields.source ?? spec.baseImageSource, - presentBefore: fields.presentBefore === "true", - presentAfter: fields.presentAfter === "true", - action: fields.action ?? null, - pullRetries: numericField(fields.pullRetries), - sourcePresentBefore: fields.sourcePresentBefore ?? null, - pulledSource: fields.pulledSource === "true", - pullAttempts: numericField(fields.pullAttempts), + target: base.target ?? spec.baseImage, + source: base.source ?? spec.baseImageSource, + presentBefore: base.presentBefore === true, + presentAfter: base.presentAfter === true, + action: base.action ?? null, + pullRetries: numericField(typeof base.pullRetries === "number" ? String(base.pullRetries) : undefined), + sourcePresentBefore: typeof base.sourcePresentBefore === "string" ? base.sourcePresentBefore : null, + pulledSource: base.pulledSource === true, + pullAttempts: numericField(typeof base.pullAttempts === "number" ? String(base.pullAttempts) : undefined), + images: imageRows, result: compactRuntimeCommand(result), }; }