// Fetch the ubuntu:24.04 linux/amd64 base image (manifest, config, layers) from Docker Hub. import { writeFile, mkdir } from 'node:fs/promises'; import { createHash } from 'node:crypto'; import { createWriteStream } from 'node:fs'; import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; const REPO = 'library/ubuntu'; const TAG = process.env.BASE_TAG || '24.04'; const OUT = process.env.OUT || '/tmp/vshell-base'; const PLATFORM = { os: 'linux', arch: 'amd64' }; const j = async (r) => { if (!r.ok) throw new Error(`${r.status} ${r.statusText} ${await r.text().catch(()=>'')}`.slice(0,300)); return r.json(); }; const tok = await j(await fetch(`https://auth.docker.io/token?service=registry.docker.io&scope=repository:${REPO}:pull`)); const H = { Authorization: `Bearer ${tok.token}`, Accept: [ 'application/vnd.oci.image.index.v1+json', 'application/vnd.docker.distribution.manifest.list.v2+json', 'application/vnd.oci.image.manifest.v1+json', 'application/vnd.docker.distribution.manifest.v2+json', ].join(', '), }; let m = await j(await fetch(`https://registry-1.docker.io/v2/${REPO}/manifests/${TAG}`, { headers: H })); if (m.manifests) { const d = m.manifests.find((x) => x.platform?.os === PLATFORM.os && x.platform?.architecture === PLATFORM.arch); if (!d) throw new Error('no amd64 manifest in index'); m = await j(await fetch(`https://registry-1.docker.io/v2/${REPO}/manifests/${d.digest}`, { headers: H })); } await mkdir(OUT, { recursive: true }); await writeFile(`${OUT}/manifest.json`, JSON.stringify(m, null, 2)); // Keep the config blob byte-for-byte: registries verify its sha256 on push, and // re-serialising the parsed JSON would not reproduce the original bytes. const cfgRes = await fetch(`https://registry-1.docker.io/v2/${REPO}/blobs/${m.config.digest}`, { headers: H }); if (!cfgRes.ok) throw new Error(`config blob: ${cfgRes.status}`); const cfgBuf = Buffer.from(await cfgRes.arrayBuffer()); const cfgDigest = 'sha256:' + createHash('sha256').update(cfgBuf).digest('hex'); if (cfgDigest !== m.config.digest) throw new Error(`config digest mismatch: ${cfgDigest} != ${m.config.digest}`); await writeFile(`${OUT}/config.raw.json`, cfgBuf); const cfg = JSON.parse(cfgBuf.toString('utf8')); await writeFile(`${OUT}/config.json`, JSON.stringify(cfg, null, 2)); const layers = []; for (const [i, l] of m.layers.entries()) { const file = `${OUT}/layer-${String(i).padStart(2, '0')}.tar`; const r = await fetch(`https://registry-1.docker.io/v2/${REPO}/blobs/${l.digest}`, { headers: H }); if (!r.ok) throw new Error(`layer ${i}: ${r.status}`); await pipeline(Readable.fromWeb(r.body), createWriteStream(file)); layers.push({ index: i, mediaType: l.mediaType, digest: l.digest, size: l.size, file }); console.log(`layer ${i} ${l.mediaType} ${l.size} ${l.digest}`); } await writeFile(`${OUT}/layers.json`, JSON.stringify({ repo: `${REPO}:${TAG}`, config: m.config, layers }, null, 2)); console.log('config.digest', m.config.digest); console.log('DONE');