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.
55 lines
3.0 KiB
JavaScript
Executable File
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');
|