diff --git a/README.md b/README.md index b468ea0..4a45960 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,22 @@ REGISTRIES_JSON = "[{ \"registry\": \"https://index.docker.io/\" }]" You can also set your `docker.io` credentials in the configuration to not have any rate-limiting. +#### Routing fallbacks by hostname + +You can route requests from different hostnames to different fallback registries. Add a `hostnames` array to each registry configuration: + +```jsonc +// wrangler.jsonc +"REGISTRIES_JSON": "[{ \"registry\": \"https://index.docker.io/\", \"hostnames\": [\"docker-mirror.example.com\"] }, { \"registry\": \"https://quay.io/\", \"hostnames\": [\"quay-mirror.example.com\"] }]" +``` + +```toml +# wrangler.toml +REGISTRIES_JSON = "[{ \"registry\": \"https://index.docker.io/\", \"hostnames\": [\"docker-mirror.example.com\"] }, { \"registry\": \"https://quay.io/\", \"hostnames\": [\"quay-mirror.example.com\"] }]" +``` + +Configurations without `hostnames` remain default fallbacks. If a request matches one or more hostname-specific configurations, the registry only tries those matches. Otherwise, it tries the default fallbacks. + ### Known limitations Right now there is some limitations with this container registry. diff --git a/src/registry/registry.ts b/src/registry/registry.ts index 217ed7e..7e62add 100644 --- a/src/registry/registry.ts +++ b/src/registry/registry.ts @@ -8,6 +8,7 @@ import { GarbageCollectionMode } from "./garbage-collector"; const registryConfiguration = z .object({ registry: z.url(), + hostnames: z.array(z.string().min(1)).min(1).optional(), }) .and( z @@ -24,14 +25,27 @@ const registryConfiguration = z ); export type RegistryConfiguration = z.infer; -export function registries(env: Env): RegistryConfiguration[] { +export function registries(env: Env, hostname?: string): RegistryConfiguration[] { if (env.REGISTRIES_JSON === undefined || env.REGISTRIES_JSON.length === 0) { return []; } try { const jsonObject = JSON.parse(env.REGISTRIES_JSON); - return registryConfiguration.array().parse(jsonObject); + const configurations = registryConfiguration.array().parse(jsonObject); + if (hostname === undefined) { + return configurations; + } + + const normalizedHostname = hostname.toLowerCase(); + const matchingConfigurations = configurations.filter((configuration) => + configuration.hostnames?.some((configuredHostname) => configuredHostname.toLowerCase() === normalizedHostname), + ); + if (matchingConfigurations.length > 0) { + return matchingConfigurations; + } + + return configurations.filter((configuration) => configuration.hostnames === undefined); } catch (err) { console.error("Error parsing registries JSON: " + errorString(err)); return []; diff --git a/src/router.ts b/src/router.ts index f8de37a..9b03b6f 100644 --- a/src/router.ts +++ b/src/router.ts @@ -153,7 +153,7 @@ v2Router.head("/:name+/manifests/:reference", async (req, env: Env) => { } let checkManifestResponse: CheckManifestResponse | null = null; - const registryList = registries(env); + const registryList = registries(env, new URL(req.url).hostname); for (const registry of registryList) { const client = new RegistryHTTPClient(env, registry); const response = await client.manifestExists(name, reference); @@ -225,7 +225,7 @@ v2Router.get("/:name+/manifests/:reference", async (req, env: Env, context: Exec } let getManifestResponse: GetManifestResponse | null = null; - const registriesList = registries(env); + const registriesList = registries(env, new URL(req.url).hostname); for (const registry of registriesList) { const client = new RegistryHTTPClient(env, registry); const response = await client.getManifest(name, reference); @@ -371,7 +371,7 @@ v2Router.get("/:name+/blobs/:digest", async (req, env: Env, context: ExecutionCo } let layerResponse: GetLayerResponse | null = null; - const registriesList = registries(env); + const registriesList = registries(env, new URL(req.url).hostname); for (const registry of registriesList) { const client = new RegistryHTTPClient(env, registry); const response = await client.getLayer(name, digest); @@ -593,7 +593,7 @@ v2Router.head("/:name+/blobs/:tag", async (req, env: Env) => { const res = await env.REGISTRY.head(`${name}/blobs/${tag}`); let layerExistsResponse: CheckLayerResponse | null = null; if (!res) { - const registryList = registries(env); + const registryList = registries(env, new URL(req.url).hostname); for (const registry of registryList) { const client = new RegistryHTTPClient(env, registry); const response = await client.layerExists(name, tag); diff --git a/test/index.test.ts b/test/index.test.ts index fffa61d..39b47ba 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -1398,6 +1398,52 @@ test("registries configuration", async () => { } }); +test("registries configuration supports hostname-specific fallbacks", () => { + const bindings = { + ...(env as Env), + REGISTRIES_JSON: JSON.stringify([ + { + registry: "https://index.docker.io/", + hostnames: ["docker-mirror.example.com"], + }, + { + registry: "https://quay.io/", + hostnames: ["quay-mirror.example.com"], + }, + { + registry: "https://docker-secondary.example.com/", + hostnames: ["docker-mirror.example.com"], + }, + { + registry: "https://default.example.com/", + }, + ]), + }; + + expect(registries(bindings, "docker-mirror.example.com")).toEqual([ + { + registry: "https://index.docker.io/", + hostnames: ["docker-mirror.example.com"], + }, + { + registry: "https://docker-secondary.example.com/", + hostnames: ["docker-mirror.example.com"], + }, + ]); + expect(registries(bindings, "QUAY-MIRROR.EXAMPLE.COM")).toEqual([ + { + registry: "https://quay.io/", + hostnames: ["quay-mirror.example.com"], + }, + ]); + expect(registries(bindings, "other.example.com")).toEqual([ + { + registry: "https://default.example.com/", + }, + ]); + expect(registries(bindings)).toHaveLength(4); +}); + describe("http client", () => { const bindings = env as Env; let envBindings = { ...bindings };