|
|
|
@@ -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),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|