Files
vshell/tools/fetch-base.mjs
T
mward4 a6f8e67976 VShell 5.0.1-3985 container image
Build the image from the vendor vshell-https .deb on ubuntu:24.04 (required:
GLIBC_2.38 / GLIBCXX_3.4.32). Contains the Dockerfile, the entrypoint that
starts and supervises the three VShell daemons without systemd, the
no-container-runtime build/validate/push tooling used to publish the image, and
the vendored .deb (sha256 pinned).

Published as gitea.milesward.dev/mward4/vshell:5.0.1-3985 and :latest.
Manifest digest sha256:8b0ff00be981fa05f1e4a4e4ff5307b6c7e5e61b487d4fbe7e02fefb2a04659f.
2026-10-05 12:15:20 -04:00

55 lines
3.0 KiB
JavaScript
Executable File

// 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');