parsear manifests de docker y limpiar lógica de parseo
This commit is contained in:
parent
4f484e58e9
commit
e9452dc659
1 changed files with 58 additions and 36 deletions
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue