diff --git a/src/lib/maintenance-actions.ts b/src/lib/maintenance-actions.ts index 261039f7b2..e1dee12cd0 100644 --- a/src/lib/maintenance-actions.ts +++ b/src/lib/maintenance-actions.ts @@ -9,6 +9,7 @@ import { normalizeGarbageCollectImagesOptions, } from "./lifecycle-options"; import { dockerListImagesFormat, dockerRmi } from "./docker"; +import { findOrphanedSandboxImages, parseSandboxImageRows } from "./maintenance-image-helpers"; import { captureOpenshell } from "./openshell-runtime"; import * as registry from "./registry"; import { parseLiveSandboxNames } from "./runtime-recovery"; @@ -83,29 +84,15 @@ export async function garbageCollectImages( process.exit(1); } - const allImages = imagesOutput - .split("\n") - .map((line: string) => line.trim()) - .filter(Boolean) - .map((line: string) => { - const [tag, size] = line.split("\t"); - return { tag, size: size || "unknown" }; - }); + const allImages = parseSandboxImageRows(imagesOutput); if (allImages.length === 0) { console.log(" No sandbox images found on the host."); return; } - const registeredTags = new Set(); const { sandboxes } = registry.listSandboxes(); - for (const sb of sandboxes) { - if (sb.imageTag) registeredTags.add(sb.imageTag); - } - - const orphans = allImages.filter( - (img: { tag: string; size: string }) => !registeredTags.has(img.tag), - ); + const orphans = findOrphanedSandboxImages(allImages, sandboxes); if (orphans.length === 0) { console.log(` All ${allImages.length} sandbox image(s) are in use. Nothing to clean up.`); diff --git a/src/lib/maintenance-image-helpers.test.ts b/src/lib/maintenance-image-helpers.test.ts new file mode 100644 index 0000000000..f79a5dd9ba --- /dev/null +++ b/src/lib/maintenance-image-helpers.test.ts @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; + +import { + findOrphanedSandboxImages, + getRegisteredImageTags, + parseSandboxImageRows, +} from "./maintenance-image-helpers"; + +describe("maintenance image helpers", () => { + it("parses Docker image rows and fills missing sizes", () => { + expect( + parseSandboxImageRows("openshell/sandbox-from:one\t1GB\nopenshell/sandbox-from:two\n\n"), + ).toEqual([ + { tag: "openshell/sandbox-from:one", size: "1GB" }, + { tag: "openshell/sandbox-from:two", size: "unknown" }, + ]); + }); + + it("collects registered sandbox image tags", () => { + expect( + getRegisteredImageTags([ + { imageTag: "openshell/sandbox-from:one" }, + { imageTag: null }, + {}, + ]), + ).toEqual(new Set(["openshell/sandbox-from:one"])); + }); + + it("finds orphaned sandbox images by registry image tags", () => { + expect( + findOrphanedSandboxImages( + [ + { tag: "openshell/sandbox-from:one", size: "1GB" }, + { tag: "openshell/sandbox-from:two", size: "2GB" }, + ], + [{ imageTag: "openshell/sandbox-from:one" }, { imageTag: null }], + ), + ).toEqual([{ tag: "openshell/sandbox-from:two", size: "2GB" }]); + }); +}); diff --git a/src/lib/maintenance-image-helpers.ts b/src/lib/maintenance-image-helpers.ts new file mode 100644 index 0000000000..78c6643a24 --- /dev/null +++ b/src/lib/maintenance-image-helpers.ts @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export type SandboxImageRow = { tag: string; size: string }; + +export function parseSandboxImageRows(imagesOutput: string): SandboxImageRow[] { + const rows: SandboxImageRow[] = []; + for (const rawLine of imagesOutput.split("\n")) { + const line = rawLine.trim(); + if (!line) continue; + const [tag, size] = line.split("\t"); + rows.push({ tag, size: size || "unknown" }); + } + return rows; +} + +export function getRegisteredImageTags( + sandboxes: Array<{ imageTag?: string | null }>, +): Set { + const registeredTags = new Set(); + for (const sandbox of sandboxes) { + if (sandbox.imageTag) registeredTags.add(sandbox.imageTag); + } + return registeredTags; +} + +export function findOrphanedSandboxImages( + images: SandboxImageRow[], + sandboxes: Array<{ imageTag?: string | null }>, +): SandboxImageRow[] { + const registeredTags = getRegisteredImageTags(sandboxes); + const orphans: SandboxImageRow[] = []; + for (const image of images) { + if (!registeredTags.has(image.tag)) { + orphans.push(image); + } + } + return orphans; +}