parsear manifests de docker y limpiar lógica de parseo

This commit is contained in:
Cat /dev/Nulo 2023-06-23 18:20:44 -03:00
parent 4f484e58e9
commit e9452dc659

View file

@ -1,8 +1,9 @@
import { execFile as _execFile } from "node:child_process"; 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 { join } from "node:path";
import { tar2squashfs } from "./tar2squashfs.js"; import { tar2squashfs } from "./tar2squashfs.js";
import { subtle } from "node:crypto"; import { subtle } from "node:crypto";
import { Writable } from "node:stream";
type RegistryName = string; type RegistryName = string;
type RegistrySecret = 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) { export async function downloadImage(image: string, tag: string) {
await mkdir(tmpDir, { recursive: true }); await mkdir(tmpDir, { recursive: true });
const manifest = await getContainerManifest(image, tag); const manifest = await getManifest(image, tag);
// sanity check // sanity check
if ( if (
@ -79,6 +80,14 @@ async function saveSquashfs(
await access(output); await access(output);
// ya está cacheado // ya está cacheado
} catch { } 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 layerStreams = manifest.layers.map(async (layer) => {
const res = await getBlob(image, layer.digest); const res = await getBlob(image, layer.digest);
return res.body!; return res.body!;
@ -131,6 +140,19 @@ async function call(registryName: string, path: string, init: RequestInit) {
return res; 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 = { type ManifestPointer = {
mediaType: "application/vnd.oci.image.manifest.v1+json"; mediaType: "application/vnd.oci.image.manifest.v1+json";
digest: string; digest: string;
@ -141,13 +163,9 @@ type ManifestPointer = {
}; };
annotations?: { [k: string]: string }; annotations?: { [k: string]: string };
}; };
type DockerManifestV2Layer = {
// https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip";
type ManifestIndex = { } & Descriptor;
mediaType: "application/vnd.oci.image.index.v1+json";
schemaVersion: 2;
manifests: ManifestPointer[];
};
function getImageParts(image: string): [string, string] { function getImageParts(image: string): [string, string] {
const parts = image.split("/"); const parts = image.split("/");
@ -156,35 +174,43 @@ function getImageParts(image: string): [string, string] {
return [registryName, imgPath]; return [registryName, imgPath];
} }
async function getContainerManifest(image: string, tag: string) { async function getManifest(image: string, tag: string): Promise<Manifest> {
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<ManifestIndex> {
const [registryName, imgPath] = getImageParts(image); const [registryName, imgPath] = getImageParts(image);
const res = await call(registryName, `/${imgPath}/manifests/${tag}`, { const res = await call(registryName, `/${imgPath}/manifests/${tag}`, {
headers: { headers: {
Accept: "application/vnd.oci.image.index.v1+json", Accept: "application/vnd.oci.image.index.v1+json",
}, },
}); });
const json = await res.json(); const json = (await res.json()) as object;
return json as ManifestIndex; 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<Manifest>(image, manifestPtr.digest);
return manifest;
} else throw new Error(`Received invalid manifest`);
} }
function chooseManifest(list: ManifestIndex, arch: string) { function chooseManifest(list: ManifestIndex, arch: string) {
@ -240,7 +266,3 @@ type Manifest = {
| "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip"; | "application/vnd.oci.image.layer.nondistributable.v1.tar+gzip";
} & Descriptor)[]; } & Descriptor)[];
}; };
async function getManifest(image: string, ptr: ManifestPointer) {
return await jsonBlob<Manifest>(image, ptr.digest);
}