Skip to content
309 linesCodeBlameRaw
1// The runner's image made in the registry (registry.mjs): its layer,
2// config and manifest, and the calls it makes, against a registry kept in
3// memory.
4import assert from "node:assert/strict";
5import { readFileSync } from "node:fs";
6import { join } from "node:path";
7import { test } from "node:test";
8import { gunzipSync } from "node:zlib";
9
10import {
11 DOCKER_MANIFEST,
12 OCI_MANIFEST,
13 RUNNER_BINARY_MODE,
14 RUNNER_BINARY_PATH,
15 RUNNER_ENTRYPOINT,
16 appendRunnerImage,
17 bearerChallenge,
18 parseRef,
19 runnerConfig,
20 runnerLayer,
21 runnerManifest,
22 sha256,
23 tarOf,
24} from "./registry.mjs";
25import { ROOT } from "./stack.mjs";
26
27/** A tar's entries: name, mode, size, owner, type and bytes, checksums checked. */
28function untar(tar) {
29 const entries = [];
30 for (let at = 0; at + 512 <= tar.length; ) {
31 const header = tar.subarray(at, at + 512);
32 if (header.every((b) => b === 0)) break;
33 const field = (start, length) => header.subarray(start, start + length).toString("ascii").replace(/\0.*$/s, "").trim();
34 const summed = [...header].reduce((sum, byte, i) => sum + (i >= 148 && i < 156 ? 32 : byte), 0);
35 assert.equal(Number.parseInt(field(148, 8), 8), summed, "the header's checksum");
36 assert.equal(field(257, 6), "ustar");
37 const size = Number.parseInt(field(124, 12), 8);
38 entries.push({
39 name: field(0, 100),
40 mode: Number.parseInt(field(100, 8), 8),
41 uid: Number.parseInt(field(108, 8), 8),
42 gid: Number.parseInt(field(116, 8), 8),
43 mtime: Number.parseInt(field(136, 12), 8),
44 type: field(156, 1),
45 data: tar.subarray(at + 512, at + 512 + size),
46 });
47 at += 512 + Math.ceil(size / 512) * 512;
48 }
49 return entries;
50}
51
52test("the runner's layer is the binary alone, at its path and mode, owned by root", () => {
53 const binary = Buffer.from("\x7fELF a runner, of a length that is not a whole block");
54 const layer = runnerLayer(binary, { mtime: 1_760_000_000 });
55 const tar = gunzipSync(layer.bytes);
56 assert.equal(tar.length % 512, 0);
57 assert.equal(layer.digest, sha256(layer.bytes));
58 assert.equal(layer.size, layer.bytes.length);
59 assert.equal(layer.diffId, sha256(tar));
60 const entries = untar(tar);
61 assert.equal(entries.length, 1);
62 const [entry] = entries;
63 assert.deepEqual(
64 { name: entry.name, mode: entry.mode, uid: entry.uid, gid: entry.gid, mtime: entry.mtime, type: entry.type },
65 { name: "usr/local/bin/g1t-runner", mode: 0o755, uid: 0, gid: 0, mtime: 1_760_000_000, type: "0" },
66 );
67 assert.ok(entry.data.equals(binary));
68 // The same bytes make the same layer.
69 assert.equal(runnerLayer(binary, { mtime: 1_760_000_000 }).digest, layer.digest);
70 // An exact number of blocks, and an empty file.
71 assert.equal(untar(tarOf("a", Buffer.alloc(512, 1)))[0].data.length, 512);
72 assert.equal(untar(tarOf("a", Buffer.alloc(0)))[0].data.length, 0);
73});
74
75test("the layer and entrypoint are what services/runner/Dockerfile builds", () => {
76 const dockerfile = readFileSync(join(ROOT, "services/runner/Dockerfile"), "utf8");
77 const copy = /^COPY --chmod=(\d+) g1t-runner (\S+)$/m.exec(dockerfile);
78 assert.ok(copy, "the Dockerfile copies the binary in");
79 assert.equal(Number.parseInt(copy[1], 8), RUNNER_BINARY_MODE);
80 assert.equal(copy[2], `/${RUNNER_BINARY_PATH}`);
81 const entrypoint = /^ENTRYPOINT (.+)$/m.exec(dockerfile);
82 assert.deepEqual(JSON.parse(entrypoint[1]), RUNNER_ENTRYPOINT);
83 // Nothing else that would change the image.
84 const instructions = dockerfile.split(/\r?\n/).filter((line) => line.trim() && !line.startsWith("#")).map((line) => line.split(" ")[0]);
85 assert.deepEqual(instructions, ["ARG", "FROM", "COPY", "ENTRYPOINT"]);
86});
87
88const BASE_CONFIG = {
89 architecture: "amd64",
90 os: "linux",
91 created: "2026-10-08T19:50:00Z",
92 config: { Env: ["PATH=/usr/local/bin:/usr/bin"], Cmd: ["bash"], WorkingDir: "/home/runner", User: "root" },
93 rootfs: { type: "layers", diff_ids: ["sha256:" + "1".repeat(64), "sha256:" + "2".repeat(64)] },
94 history: [{ created_by: "FROM debian" }, { created_by: "RUN apt-get install" }],
95};
96
97test("the config is the base's, with the layer, the entrypoint and no inherited command", () => {
98 const before = structuredClone(BASE_CONFIG);
99 const diffId = "sha256:" + "3".repeat(64);
100 const config = runnerConfig(BASE_CONFIG, { diffId, created: "2026-10-13T00:00:00.000Z" });
101 assert.deepEqual(BASE_CONFIG, before, "the base's config is left as it was");
102 assert.deepEqual(config.config.Entrypoint, ["g1t-runner"]);
103 assert.equal(config.config.Cmd, undefined);
104 assert.deepEqual(config.config.Env, BASE_CONFIG.config.Env);
105 assert.equal(config.config.WorkingDir, "/home/runner");
106 assert.deepEqual(config.rootfs.diff_ids, [...BASE_CONFIG.rootfs.diff_ids, diffId]);
107 assert.equal(config.created, "2026-10-13T00:00:00.000Z");
108 // History: one entry per layer, plus one that adds none.
109 assert.equal(config.history.filter((h) => !h.empty_layer).length - BASE_CONFIG.history.length, 1);
110 assert.equal(config.history.at(-1).empty_layer, true);
111 assert.throws(() => runnerConfig({ ...BASE_CONFIG, architecture: "arm64" }, { diffId, created: "x" }), /linux\/amd64/);
112});
113
114test("the manifest names the base's layers, then the binary's, in the base's format", () => {
115 const layers = [
116 { mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", digest: "sha256:" + "a".repeat(64), size: 10 },
117 { mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", digest: "sha256:" + "b".repeat(64), size: 20 },
118 ];
119 const config = { digest: "sha256:" + "c".repeat(64), size: 5 };
120 const layer = { digest: "sha256:" + "d".repeat(64), size: 7 };
121 const docker = runnerManifest(
122 { schemaVersion: 2, mediaType: DOCKER_MANIFEST, config: { mediaType: "application/vnd.docker.container.image.v1+json", digest: "x", size: 1 }, layers },
123 { config, layer },
124 );
125 assert.deepEqual(docker, {
126 schemaVersion: 2,
127 mediaType: DOCKER_MANIFEST,
128 config: { mediaType: "application/vnd.docker.container.image.v1+json", ...config },
129 layers: [...layers, { mediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", ...layer }],
130 });
131 // OCI, with its type left unsaid.
132 const oci = runnerManifest(
133 { schemaVersion: 2, config: { mediaType: "application/vnd.oci.image.config.v1+json", digest: "x", size: 1 }, layers: [], annotations: { a: "b" } },
134 { config, layer },
135 );
136 assert.equal(oci.mediaType, OCI_MANIFEST);
137 assert.deepEqual(oci.layers, [{ mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", ...layer }]);
138 assert.equal(oci.annotations, undefined);
139});
140
141test("image references and bearer challenges are read", () => {
142 const digest = "sha256:" + "e".repeat(64);
143 assert.deepEqual(parseRef("registry.cloudflare.com/acct/g1t-runner:abc"), { host: "registry.cloudflare.com", name: "acct/g1t-runner", tag: "abc", digest: null });
144 assert.deepEqual(parseRef(`localhost:5000/g1t-runner:base-1@${digest}`), { host: "localhost:5000", name: "g1t-runner", tag: "base-1", digest });
145 assert.deepEqual(parseRef(`r.example/x@${digest}`), { host: "r.example", name: "x", tag: null, digest });
146 assert.throws(() => parseRef("r.example/x"));
147 assert.deepEqual(bearerChallenge('Bearer realm="https://auth.example/token",service="registry",scope="repository:x:pull"'), {
148 realm: "https://auth.example/token",
149 service: "registry",
150 scope: "repository:x:pull",
151 });
152 assert.equal(bearerChallenge('Basic realm="x"'), null);
153 assert.equal(bearerChallenge(null), null);
154});
155
156/** A registry in memory: one repository, Basic auth, uploads by POST, PATCH and PUT. */
157function fakeRegistry({ name, auth = "Basic " + Buffer.from("u:p").toString("base64") }) {
158 const blobs = new Map();
159 const manifests = new Map();
160 const uploads = new Map();
161 const calls = [];
162 let next = 0;
163 const reply = (status, { headers = {}, body = null } = {}) => ({
164 status,
165 ok: status >= 200 && status < 300,
166 headers: new Headers(headers),
167 arrayBuffer: async () => (body ? body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength) : new ArrayBuffer(0)),
168 text: async () => (body ? body.toString("utf8") : ""),
169 json: async () => JSON.parse(body.toString("utf8")),
170 });
171 const fetchImpl = async (url, init) => {
172 const { pathname, searchParams } = new URL(url);
173 calls.push(`${init.method} ${pathname}`);
174 if (init.headers.authorization !== auth) return reply(401);
175 const prefix = `/v2/${name}/`;
176 if (!pathname.startsWith(prefix)) return reply(404);
177 const path = pathname.slice(prefix.length);
178 let match;
179 if ((match = /^manifests\/(.+)$/.exec(path))) {
180 if (init.method === "PUT") {
181 const bytes = Buffer.from(init.body);
182 const digest = sha256(bytes);
183 for (const ref of [...JSON.parse(bytes).layers, JSON.parse(bytes).config]) {
184 if (!blobs.has(ref.digest)) return reply(400, { body: Buffer.from('{"errors":[{"code":"MANIFEST_BLOB_UNKNOWN"}]}') });
185 }
186 manifests.set(match[1], { bytes, type: init.headers["content-type"] });
187 manifests.set(digest, { bytes, type: init.headers["content-type"] });
188 return reply(201, { headers: { "docker-content-digest": digest } });
189 }
190 const found = manifests.get(match[1]);
191 return found ? reply(200, { headers: { "content-type": found.type }, body: found.bytes }) : reply(404);
192 }
193 if ((match = /^blobs\/(sha256:[0-9a-f]{64})$/.exec(path))) {
194 const found = blobs.get(match[1]);
195 if (!found) return reply(404);
196 return init.method === "HEAD" ? reply(200) : reply(200, { body: found });
197 }
198 if (path === "blobs/uploads/" && init.method === "POST") {
199 const id = `u${next++}`;
200 uploads.set(id, Buffer.alloc(0));
201 return reply(202, { headers: { location: `${prefix}blobs/uploads/${id}` } });
202 }
203 if ((match = /^blobs\/uploads\/(u\d+)$/.exec(path))) {
204 const id = match[1];
205 if (init.method === "PATCH") {
206 uploads.set(id, Buffer.concat([uploads.get(id), Buffer.from(init.body)]));
207 return reply(202, { headers: { location: `${prefix}blobs/uploads/${id}?state=1` } });
208 }
209 if (init.method === "PUT") {
210 const bytes = uploads.get(id);
211 if (sha256(bytes) !== searchParams.get("digest")) return reply(400);
212 blobs.set(searchParams.get("digest"), bytes);
213 return reply(201);
214 }
215 }
216 return reply(405);
217 };
218 return { blobs, manifests, calls, fetchImpl };
219}
220
221test("the runner's image is made in the registry from the base's manifest and one new layer, pulling none of the base", async () => {
222 const registry = fakeRegistry({ name: "acct/g1t-runner" });
223 // The base, as `docker push` left it.
224 const layerBlobs = [Buffer.from("base layer one"), Buffer.from("base layer two")];
225 for (const blob of layerBlobs) registry.blobs.set(sha256(blob), blob);
226 const configBytes = Buffer.from(JSON.stringify(BASE_CONFIG));
227 registry.blobs.set(sha256(configBytes), configBytes);
228 const baseManifest = Buffer.from(
229 JSON.stringify({
230 schemaVersion: 2,
231 mediaType: OCI_MANIFEST,
232 config: { mediaType: "application/vnd.oci.image.config.v1+json", digest: sha256(configBytes), size: configBytes.length },
233 layers: layerBlobs.map((blob) => ({ mediaType: "application/vnd.oci.image.layer.v1.tar+gzip", digest: sha256(blob), size: blob.length })),
234 }),
235 );
236 registry.manifests.set(sha256(baseManifest), { bytes: baseManifest, type: OCI_MANIFEST });
237 const base = `registry.cloudflare.com/acct/g1t-runner:base-20261008-a314499af03b@${sha256(baseManifest)}`;
238 const ref = "registry.cloudflare.com/acct/g1t-runner:0123456789abcdef";
239 const binary = Buffer.from("the runner binary");
240 const credentials = async () => ({ username: "u", password: "p" });
241
242 const digest = await appendRunnerImage({ ref, base, binary, credentials, fetchImpl: registry.fetchImpl, created: "2026-10-13T00:00:00.000Z" });
243 const made = registry.manifests.get("0123456789abcdef");
244 assert.equal(sha256(made.bytes), digest);
245 assert.equal(made.type, OCI_MANIFEST);
246 const manifest = JSON.parse(made.bytes);
247 assert.deepEqual(manifest.layers.slice(0, 2).map((l) => l.digest), layerBlobs.map(sha256));
248 assert.equal(manifest.layers.length, 3);
249 const config = JSON.parse(registry.blobs.get(manifest.config.digest));
250 assert.deepEqual(config.config.Entrypoint, ["g1t-runner"]);
251 assert.equal(config.rootfs.diff_ids.at(-1), sha256(gunzipSync(registry.blobs.get(manifest.layers[2].digest))));
252 assert.ok(untar(gunzipSync(registry.blobs.get(manifest.layers[2].digest)))[0].data.equals(binary));
253 // The base's layers were asked about, never downloaded.
254 for (const blob of layerBlobs) assert.ok(!registry.calls.includes(`GET /v2/acct/g1t-runner/blobs/${sha256(blob)}`));
255
256 // Made again: the layer is already there, so only the config and manifest are written.
257 const before = registry.calls.length;
258 await appendRunnerImage({ ref, base, binary, credentials, fetchImpl: registry.fetchImpl, created: "2026-10-13T00:00:00.000Z" });
259 assert.ok(!registry.calls.slice(before).some((call) => call.startsWith("PATCH")));
260});
261
262test("a base that is not where it should be, or not what base.json says, stops it", async () => {
263 const registry = fakeRegistry({ name: "acct/g1t-runner" });
264 const credentials = async () => ({ username: "u", password: "p" });
265 const digest = "sha256:" + "f".repeat(64);
266 const options = { binary: Buffer.from("x"), credentials, fetchImpl: registry.fetchImpl, attempts: 1 };
267 await assert.rejects(
268 appendRunnerImage({ ...options, ref: "registry.cloudflare.com/acct/g1t-runner:t", base: `registry.cloudflare.com/acct/other:b@${digest}` }),
269 /not in the runner's repository/,
270 );
271 await assert.rejects(appendRunnerImage({ ...options, ref: "registry.cloudflare.com/acct/g1t-runner:t", base: `registry.cloudflare.com/acct/g1t-runner:b@${digest}` }), /404/);
272 // Wrong credentials: the registry's answer is the error.
273 await assert.rejects(
274 appendRunnerImage({ ...options, credentials: async () => ({ username: "u", password: "wrong" }), ref: "registry.cloudflare.com/acct/g1t-runner:t", base: `registry.cloudflare.com/acct/g1t-runner:b@${digest}` }),
275 /401/,
276 );
277});
278
279test("a registry that asks for a bearer token is given the one its realm trades the credentials for", async () => {
280 const registry = fakeRegistry({ name: "acct/r", auth: "Bearer tok" });
281 const inner = registry.fetchImpl;
282 const asked = [];
283 const fetchImpl = async (url, init) => {
284 if (url.startsWith("https://auth.example/")) {
285 asked.push([url, init.headers.authorization]);
286 return { ok: true, status: 200, json: async () => ({ token: "tok" }) };
287 }
288 const answer = await inner(url, init);
289 if (answer.status === 401) answer.headers.set("www-authenticate", 'Bearer realm="https://auth.example/token",service="r.example"');
290 return answer;
291 };
292 const config = Buffer.from(JSON.stringify(BASE_CONFIG));
293 registry.blobs.set(sha256(config), config);
294 const manifest = Buffer.from(
295 JSON.stringify({ schemaVersion: 2, mediaType: DOCKER_MANIFEST, config: { mediaType: "application/vnd.docker.container.image.v1+json", digest: sha256(config), size: config.length }, layers: [] }),
296 );
297 registry.manifests.set("base", { bytes: manifest, type: DOCKER_MANIFEST });
298 await appendRunnerImage({
299 ref: "r.example/acct/r:new",
300 base: "r.example/acct/r:base",
301 binary: Buffer.from("x"),
302 credentials: async () => ({ username: "u", password: "p" }),
303 fetchImpl,
304 });
305 assert.ok(registry.manifests.has("new"));
306 assert.equal(asked.length, 1, "one token, kept");
307 assert.equal(asked[0][0], "https://auth.example/token?service=r.example&scope=repository%3Aacct%2Fr%3Apull%2Cpush");
308 assert.equal(asked[0][1], `Basic ${Buffer.from("u:p").toString("base64")}`);
309});