import { execFile as _execFile } from "node:child_process"; import { mkdtemp, open, rm } from "node:fs/promises"; import { join } from "node:path"; import { Writable } from "node:stream"; import { promisify } from "node:util"; import { tar2squashfs } from "./tar2squashfs.js"; const execFile = promisify(_execFile); type RegistryName = string; type RegistrySecret = string; let tokenCache = new Map>(); const tmp = await mkdtemp("sexy-chambelan-"); try { console.debug(tmp); const image = "gitea.nulo.in/nulo/zulip-checkin-cyborg"; const tag = "latest"; const arch = "amd64"; const manifest = await getContainerManifest(image, tag); if ( !manifest.layers.every( (layer) => layer.mediaType === "application/vnd.oci.image.layer.v1.tar" || layer.mediaType === "application/vnd.oci.image.layer.v1.tar+gzip" ) ) { throw new Error("Unsupported layer"); } console.debug(manifest); const layers = await Promise.all( manifest.layers.map(async (layer) => { const path = join(tmp, layer.digest); await downloadBlob(image, layer.digest, path); return { layer, path }; }) ); const squashfs = `${image.replaceAll("/", "%")}.squashfs`; await rm(squashfs, { force: true }); await tar2squashfs( layers.map(async ({ path }) => { const f = await open(path); return f.createReadStream(); }), squashfs ); await downloadBlob( image, manifest.config.digest, `${image.replaceAll("/", "%")}.config.json` ); process.exit(1); } finally { await rm(tmp, { recursive: true }); } async function _getToken(registryUrl: string): Promise { const res = await fetch(`${registryUrl}/token`); const json = await res.json(); if ( !json || typeof json !== "object" || !("token" in json) || typeof json.token !== "string" ) throw new Error("Unexpected"); return json.token; } async function call(registryName: string, path: string, init: RequestInit) { const registryUrl = `https://${registryName}/v2`; let tokenPromise = tokenCache.get(registryName); if (!tokenPromise) { tokenPromise = _getToken(registryUrl); tokenCache.set(registryName, tokenPromise); } const token = await tokenPromise; console.debug(path); const res = await fetch(`${registryUrl}${path}`, { ...init, headers: { ...init.headers, Authorization: `Bearer ${token}`, }, }); if (!res.ok) throw new Error(res.statusText); return res; } type ManifestPointer = { mediaType: "application/vnd.oci.image.manifest.v1+json"; digest: string; size: number; platform: { architecture: "amd64" | string; os: string; }; 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[]; }; function getImageParts(image: string): [string, string] { const parts = image.split("/"); const registryName = parts[0]; const imgPath = parts.slice(1).join("/"); 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 { 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; } function chooseManifest(list: ManifestIndex, arch: string) { console.debug(list); return list.manifests.find( (m) => m.platform.architecture === arch && m.platform.os === "linux" ); } async function getBlob(image: string, digest: string): Promise { const [registryName, imgPath] = getImageParts(image); return await call(registryName, `/${imgPath}/blobs/${digest}`, {}); } async function jsonBlob(image: string, digest: string): Promise { const res = await getBlob(image, digest); return (await res.json()) as T; } async function downloadBlob( image: string, digest: string, outputPath: string ): Promise { const res = await getBlob(image, digest); const file = await open(outputPath, "w"); await res.body!.pipeTo(Writable.toWeb(file.createWriteStream())); return; } // https://github.com/opencontainers/image-spec/blob/v1.0.1/descriptor.md#properties type Descriptor = { digest: string; size: number; urls?: string[]; annotations?: { [k: string]: string }; }; // https://github.com/opencontainers/image-spec/blob/v1.0.1/manifest.md type Manifest = { mediaType: "application/vnd.oci.image.manifest.v1+json"; config: { mediaType: "application/vnd.oci.image.config.v1+json"; } & Descriptor; layers: ({ mediaType: | "application/vnd.oci.image.layer.v1.tar" | "application/vnd.oci.image.layer.v1.tar+gzip" | "application/vnd.oci.image.layer.nondistributable.v1.tar" | "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"; } & Descriptor)[]; }; async function getManifest(image: string, ptr: ManifestPointer) { return await jsonBlob(image, ptr.digest); }