From e9452dc659ecbce97b69f10d5130d9c172e7afee Mon Sep 17 00:00:00 2001 From: Nulo Date: Fri, 23 Jun 2023 18:20:44 -0300 Subject: [PATCH] =?UTF-8?q?parsear=20manifests=20de=20docker=20y=20limpiar?= =?UTF-8?q?=20l=C3=B3gica=20de=20parseo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/container-baby.ts | 94 +++++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/server/container-baby.ts b/server/container-baby.ts index e9f1c17..ff7a846 100644 --- a/server/container-baby.ts +++ b/server/container-baby.ts @@ -1,8 +1,9 @@ import { execFile as _execFile } from "node:child_process"; -import { access, mkdir, writeFile } from "node:fs/promises"; +import { access, mkdir, open, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tar2squashfs } from "./tar2squashfs.js"; import { subtle } from "node:crypto"; +import { Writable } from "node:stream"; type RegistryName = string; type RegistrySecret = string; @@ -27,7 +28,7 @@ export function parseImageRef(ref: string): { image: string; tag: string } { export async function downloadImage(image: string, tag: string) { await mkdir(tmpDir, { recursive: true }); - const manifest = await getContainerManifest(image, tag); + const manifest = await getManifest(image, tag); // sanity check if ( @@ -79,6 +80,14 @@ async function saveSquashfs( await access(output); // ya está cacheado } catch { + if (process.env.DEBUG_WRITE_BLOBS) + await Promise.all( + manifest.layers.map(async (layer) => { + const res = await getBlob(image, layer.digest); + const file = await open(layer.digest, "w"); + await res.body!.pipeTo(Writable.toWeb(file.createWriteStream())); + }) + ); const layerStreams = manifest.layers.map(async (layer) => { const res = await getBlob(image, layer.digest); return res.body!; @@ -131,6 +140,19 @@ async function call(registryName: string, path: string, init: RequestInit) { return res; } +// https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md +type ManifestIndex = { + schemaVersion: 2; + mediaType: "application/vnd.oci.image.index.v1+json"; + manifests: ManifestPointer[]; +}; +type DockerManifestV2 = { + schemaVersion: 2; + mediaType: "application/vnd.docker.distribution.manifest.v2+json"; + config: Descriptor; + layers: DockerManifestV2Layer[]; +}; + type ManifestPointer = { mediaType: "application/vnd.oci.image.manifest.v1+json"; digest: string; @@ -141,13 +163,9 @@ type ManifestPointer = { }; annotations?: { [k: string]: string }; }; - -// https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md -type ManifestIndex = { - mediaType: "application/vnd.oci.image.index.v1+json"; - schemaVersion: 2; - manifests: ManifestPointer[]; -}; +type DockerManifestV2Layer = { + mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip"; +} & Descriptor; function getImageParts(image: string): [string, string] { const parts = image.split("/"); @@ -156,35 +174,43 @@ function getImageParts(image: string): [string, string] { return [registryName, imgPath]; } -async function getContainerManifest(image: string, tag: string) { - const index = await getManifestIndex(image, tag); - // badly behaved registries will return whatever they want - if ( - (index.mediaType as string) === "application/vnd.oci.image.manifest.v1+json" - ) { - return index as unknown as Manifest; - } - const arch = "amd64"; - const ptr = chooseManifest(index, arch); - if (!ptr) - throw new Error(`Image ${image}:${tag} doesn't exist for arch ${arch}`); - const manifest = await getManifest(image, ptr); - return manifest; -} - -async function getManifestIndex( - image: string, - tag: string -): Promise { +async function getManifest(image: string, tag: string): Promise { const [registryName, imgPath] = getImageParts(image); - const res = await call(registryName, `/${imgPath}/manifests/${tag}`, { headers: { Accept: "application/vnd.oci.image.index.v1+json", }, }); - const json = await res.json(); - return json as ManifestIndex; + const json = (await res.json()) as object; + if (!("mediaType" in json)) throw new Error(`Received invalid manifest`); + + if ( + json.mediaType === "application/vnd.docker.distribution.manifest.v2+json" + ) { + const manifest = json as DockerManifestV2; + return { + mediaType: "application/vnd.oci.image.manifest.v1+json", + config: { + ...manifest.config, + mediaType: "application/vnd.oci.image.config.v1+json", + }, + layers: manifest.layers.map((l) => ({ + ...l, + mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", + })), + }; + } else if (json.mediaType === "application/vnd.oci.image.manifest.v1+json") { + return json as Manifest; + } else if (json.mediaType === "application/vnd.oci.image.index.v1+json") { + const index = json as ManifestIndex; + // XXX: hardcoded amd64 + const arch = "amd64"; + const manifestPtr = chooseManifest(index, arch); + if (!manifestPtr) throw new Error(`No manifest for arch ${arch}`); + + const manifest = await jsonBlob(image, manifestPtr.digest); + return manifest; + } else throw new Error(`Received invalid manifest`); } function chooseManifest(list: ManifestIndex, arch: string) { @@ -240,7 +266,3 @@ type Manifest = { | "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"; } & Descriptor)[]; }; - -async function getManifest(image: string, ptr: ManifestPointer) { - return await jsonBlob(image, ptr.digest); -}