| 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. |
| 4 | import assert from "node:assert/strict"; |
| 5 | import { readFileSync } from "node:fs"; |
| 6 | import { join } from "node:path"; |
| 7 | import { test } from "node:test"; |
| 8 | import { gunzipSync } from "node:zlib"; |
| 9 | |
| 10 | import { |
| 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"; |
| 25 | import { ROOT } from "./stack.mjs"; |
| 26 | |
| 27 | /** A tar's entries: name, mode, size, owner, type and bytes, checksums checked. */ |
| 28 | function 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 | |
| 52 | test("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 | |
| 75 | test("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 | |
| 88 | const 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 | |
| 97 | test("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 | |
| 114 | test("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 | |
| 141 | test("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. */ |
| 157 | function 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 | |
| 221 | test("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 | |
| 262 | test("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 | |
| 279 | test("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 | }); |