fix: manage hwlab buildkit runtime image

This commit is contained in:
Codex
2026-06-27 12:56:13 +00:00
parent 37a31e8416
commit 92b62a5e50
5 changed files with 142 additions and 88 deletions
+2
View File
@@ -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
+2
View File
@@ -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),
};
}
+3 -3
View File
@@ -174,7 +174,7 @@ export function nodeRuntimeBaseImageCommand(scoped: ReturnType<typeof parseNodeS
mode: "status",
mutation: false,
status: statusBefore,
degradedReason: statusBefore.ok ? undefined : "node-runtime-base-image-not-ready",
degradedReason: statusBefore.ok ? undefined : "node-runtime-image-dependency-not-ready",
next: statusBefore.ok ? undefined : {
preload: `bun scripts/cli.ts hwlab nodes control-plane runtime-image preload --node ${scoped.node} --lane ${scoped.lane} --confirm`,
},
@@ -196,10 +196,10 @@ export function nodeRuntimeBaseImageCommand(scoped: ReturnType<typeof parseNodeS
preload,
statusAfter,
degradedReason: preload === null
? "node-runtime-base-image-source-missing"
? "node-runtime-image-source-missing"
: preload.ok === true && (scoped.dryRun || statusAfter.ok === true)
? undefined
: "node-runtime-base-image-seed-failed",
: "node-runtime-image-seed-failed",
next: scoped.dryRun
? { preload: `bun scripts/cli.ts hwlab nodes control-plane runtime-image preload --node ${scoped.node} --lane ${scoped.lane} --confirm` }
: { triggerCurrent: `bun scripts/cli.ts hwlab nodes control-plane trigger-current --node ${scoped.node} --lane ${scoped.lane} --confirm --rerun` },
+1
View File
@@ -116,6 +116,7 @@ export function nodeRuntimeExpected(spec: HwlabRuntimeLaneSpec): Record<string,
serviceIds: spec.serviceIds,
buildkit: spec.buildkit === undefined ? null : {
sidecarImage: spec.buildkit.sidecarImage,
sourceImage: spec.buildkit.sourceImage,
},
dockerBuildProxy: {
http: spec.networkProfile.dockerBuildProxy.http,
+134 -85
View File
@@ -805,44 +805,81 @@ export function localPostgresBootstrapSecretScript(spec: HwlabRuntimeLaneSpec, s
].join("\n");
}
interface NodeRuntimeImageDependency {
id: string;
target: string;
source: string;
}
function nodeRuntimeImageDependencies(spec: HwlabRuntimeLaneSpec): NodeRuntimeImageDependency[] {
const images: NodeRuntimeImageDependency[] = [
{ id: "base", target: spec.baseImage, source: spec.baseImageSource ?? "" },
];
if (spec.buildkit !== undefined) {
images.push({ id: "buildkit", target: spec.buildkit.sidecarImage, source: spec.buildkit.sourceImage });
}
return images;
}
function nodeRuntimeImageCalls(spec: HwlabRuntimeLaneSpec, functionName: string): string {
return nodeRuntimeImageDependencies(spec)
.map((image) => `${functionName} ${shellQuote(image.id)} ${shellQuote(image.target)} ${shellQuote(image.source)}`)
.join("\n");
}
function parseNodeRuntimeImageRows(text: string): Record<string, unknown>[] {
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<string, unknown>];
} catch {
return [];
}
});
}
export function nodeRuntimeBaseImageStatus(spec: HwlabRuntimeLaneSpec, timeoutSeconds: number): Record<string, unknown> {
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),
};
}