Skip to content

Commit c107fc0

Browse files
committed
pull fallback: allow anonymous auth with docker.io and upload manifest from fallback should work
1 parent 518ea48 commit c107fc0

File tree

5 files changed

+90
-37
lines changed

5 files changed

+90
-37
lines changed

README.md

+8
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ the target registry and setup the credentials.
110110

111111
**Never put a registry password/token inside the wrangler.toml, please always use `wrangler secrets put`**
112112

113+
You can also use docker.io with anonymous authentication:
114+
```
115+
REGISTRIES_JSON = "[{ \"registry\": \"https://index.docker.io/\" }]"
116+
```
117+
118+
You can also set your `docker.io` credentials in the configuration to not have any rate-limiting.
119+
120+
113121
### Known limitations
114122

115123
Right now there is some limitations with this container registry.

src/registry/http.ts

+36-19
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ type AuthContext = {
2424
scope: string;
2525
};
2626

27+
function isDockerDotIO(url: URL): boolean {
28+
return url.host.endsWith("docker.io");
29+
}
30+
2731
type HTTPContext = {
2832
// The auth context for this request
2933
authContext: AuthContext;
@@ -149,14 +153,35 @@ export class RegistryHTTPClient implements Registry {
149153
}
150154

151155
authBase64(): string {
156+
const configuration = this.configuration;
157+
if (configuration.username === undefined) {
158+
return "";
159+
}
160+
152161
return btoa(this.configuration.username + ":" + this.password());
153162
}
154163

155164
password(): string {
156-
return (this.env as unknown as Record<string, string>)[this.configuration.password_env] ?? "";
165+
const configuration = this.configuration;
166+
if (configuration.username === undefined) {
167+
return "";
168+
}
169+
170+
return (this.env as unknown as Record<string, string>)[configuration.password_env] ?? "";
157171
}
158172

159173
async authenticate(namespace: string): Promise<HTTPContext> {
174+
const emptyAuthentication = {
175+
authContext: {
176+
authType: "none",
177+
realm: "",
178+
scope: "",
179+
service: this.url.host,
180+
},
181+
repository: this.url.pathname,
182+
accessToken: "",
183+
} as const;
184+
160185
const res = await fetch(`${this.url.protocol}//${this.url.host}/v2/`, {
161186
headers: {
162187
"User-Agent": "Docker-Client/24.0.5 (linux)",
@@ -165,16 +190,7 @@ export class RegistryHTTPClient implements Registry {
165190
});
166191

167192
if (res.ok) {
168-
return {
169-
authContext: {
170-
authType: "none",
171-
realm: "",
172-
scope: "",
173-
service: this.url.host,
174-
},
175-
repository: this.url.pathname,
176-
accessToken: "",
177-
};
193+
return emptyAuthentication;
178194
}
179195

180196
if (res.status !== 401) {
@@ -212,11 +228,12 @@ export class RegistryHTTPClient implements Registry {
212228
async authenticateBearerSimple(ctx: AuthContext, params: URLSearchParams): Promise<Response> {
213229
params.delete("password");
214230
console.log("sending authentication parameters:", ctx.realm + "?" + params.toString());
231+
215232
return await fetch(ctx.realm + "?" + params.toString(), {
216233
headers: {
217-
"Authorization": "Basic " + this.authBase64(),
218234
"Accept": "application/json",
219235
"User-Agent": "Docker-Client/24.0.5 (linux)",
236+
...(this.configuration.username !== undefined ? { Authorization: "Basic " + this.authBase64() } : {}),
220237
},
221238
});
222239
}
@@ -227,8 +244,8 @@ export class RegistryHTTPClient implements Registry {
227244
// explicitely include that we don't want an offline_token.
228245
scope: `repository:${ctx.scope}:pull,push`,
229246
client_id: "r2registry",
230-
grant_type: "password",
231-
password: this.password(),
247+
grant_type: this.configuration.username === undefined ? "none" : "password",
248+
password: this.configuration.username === undefined ? "" : this.password(),
232249
});
233250
let response = await fetch(ctx.realm, {
234251
headers: {
@@ -313,7 +330,7 @@ export class RegistryHTTPClient implements Registry {
313330
}
314331

315332
async manifestExists(name: string, tag: string): Promise<CheckManifestResponse | RegistryError> {
316-
const namespace = name.includes("/") ? name : `library/${name}`;
333+
const namespace = name.includes("/") || !isDockerDotIO(this.url) ? name : `library/${name}`;
317334
try {
318335
const ctx = await this.authenticate(namespace);
319336
const req = ctxIntoRequest(ctx, this.url, "HEAD", `${namespace}/manifests/${tag}`);
@@ -342,7 +359,7 @@ export class RegistryHTTPClient implements Registry {
342359
}
343360

344361
async getManifest(name: string, digest: string): Promise<GetManifestResponse | RegistryError> {
345-
const namespace = name.includes("/") ? name : `library/${name}`;
362+
const namespace = name.includes("/") || !isDockerDotIO(this.url) ? name : `library/${name}`;
346363
try {
347364
const ctx = await this.authenticate(namespace);
348365
const req = ctxIntoRequest(ctx, this.url, "GET", `${namespace}/manifests/${digest}`);
@@ -374,7 +391,7 @@ export class RegistryHTTPClient implements Registry {
374391
}
375392

376393
async layerExists(name: string, digest: string): Promise<CheckLayerResponse | RegistryError> {
377-
const namespace = name.includes("/") ? name : `library/${name}`;
394+
const namespace = name.includes("/") || !isDockerDotIO(this.url) ? name : `library/${name}`;
378395
try {
379396
const ctx = await this.authenticate(namespace);
380397
const res = await fetch(ctxIntoRequest(ctx, this.url, "HEAD", `${namespace}/blobs/${digest}`));
@@ -414,7 +431,7 @@ export class RegistryHTTPClient implements Registry {
414431
}
415432

416433
async getLayer(name: string, digest: string): Promise<GetLayerResponse | RegistryError> {
417-
const namespace = name.includes("/") ? name : `library/${name}`;
434+
const namespace = name.includes("/") || !isDockerDotIO(this.url) ? name : `library/${name}`;
418435
try {
419436
const ctx = await this.authenticate(namespace);
420437
const req = ctxIntoRequest(ctx, this.url, "GET", `${namespace}/blobs/${digest}`);
@@ -468,7 +485,7 @@ export class RegistryHTTPClient implements Registry {
468485
_namespace: string,
469486
_reference: string,
470487
_readableStream: ReadableStream<any>,
471-
_contentType: string,
488+
{}: {},
472489
): Promise<PutManifestResponse | RegistryError> {
473490
throw new Error("unimplemented");
474491
}

src/registry/r2.ts

+20-5
Original file line numberDiff line numberDiff line change
@@ -292,11 +292,23 @@ export class R2Registry implements Registry {
292292
namespace: string,
293293
reference: string,
294294
readableStream: ReadableStream<any>,
295-
contentType: string,
295+
{
296+
contentType,
297+
checkLayers,
298+
}: {
299+
contentType: string;
300+
checkLayers?: boolean;
301+
},
296302
): Promise<PutManifestResponse | RegistryError> {
297303
const key = await this.gc.markForInsertion(namespace);
298304
try {
299-
return this.putManifestInner(namespace, reference, readableStream, contentType);
305+
return this.putManifestInner(
306+
namespace,
307+
reference,
308+
readableStream,
309+
contentType,
310+
checkLayers !== undefined ? checkLayers === true : true,
311+
);
300312
} finally {
301313
// if this fails, at some point it will be expired
302314
await this.gc.cleanInsertion(namespace, key);
@@ -308,6 +320,7 @@ export class R2Registry implements Registry {
308320
reference: string,
309321
readableStream: ReadableStream<any>,
310322
contentType: string,
323+
checkLayers: boolean,
311324
): Promise<PutManifestResponse | RegistryError> {
312325
const gcMarker = await this.gc.getGCMarker(name);
313326
const env = this.env;
@@ -322,8 +335,10 @@ export class R2Registry implements Registry {
322335
const text = await blob.text();
323336
const manifestJSON = JSON.parse(text);
324337
const manifest = manifestSchema.parse(manifestJSON);
325-
const verifyManifestErr = await this.verifyManifest(name, manifest);
326-
if (verifyManifestErr !== null) return { response: verifyManifestErr };
338+
if (checkLayers) {
339+
const verifyManifestErr = await this.verifyManifest(name, manifest);
340+
if (verifyManifestErr !== null) return { response: verifyManifestErr };
341+
}
327342

328343
if (!(await this.gc.checkCanInsertData(name, gcMarker))) {
329344
console.error("Manifest can't be uploaded as there is/was a garbage collection going");
@@ -774,7 +789,7 @@ export class R2Registry implements Registry {
774789
if (hashedState === null || !hashedState.state) {
775790
return {
776791
response: new Response(null, { status: 404 }),
777-
}
792+
};
778793
}
779794
const state = hashedState.state;
780795

src/registry/registry.ts

+16-7
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,18 @@ import z from "zod";
55
import { GarbageCollectionMode } from "./garbage-collector";
66

77
// Defines a registry and how it's configured
8-
const registryConfiguration = z.object({
9-
registry: z.string().url(),
10-
password_env: z.string(),
11-
username: z.string(),
12-
});
13-
8+
const registryConfiguration = z
9+
.object({
10+
registry: z.string().url(),
11+
})
12+
.and(
13+
z
14+
.object({
15+
password_env: z.string(),
16+
username: z.string(),
17+
})
18+
.or(z.undefined()),
19+
);
1420
export type RegistryConfiguration = z.infer<typeof registryConfiguration>;
1521

1622
export function registries(env: Env): RegistryConfiguration[] {
@@ -132,7 +138,10 @@ export interface Registry {
132138
namespace: string,
133139
reference: string,
134140
readableStream: ReadableStream<any>,
135-
contentType: string,
141+
options: {
142+
contentType: string;
143+
checkLayers?: boolean;
144+
},
136145
): Promise<PutManifestResponse | RegistryError>;
137146

138147
// starts a new upload

src/router.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,10 @@ v2Router.head("/:name+/manifests/:reference", async (req, env: Env) => {
149149
}
150150

151151
const [putResponse, err] = await wrap(
152-
env.REGISTRY_CLIENT.putManifest(name, reference, manifestResponse.stream, manifestResponse.contentType),
152+
env.REGISTRY_CLIENT.putManifest(name, reference, manifestResponse.stream, {
153+
contentType: manifestResponse.contentType,
154+
checkLayers: false,
155+
}),
153156
);
154157
if (err) {
155158
console.error("Error sync manifest into client:", errorString(err));
@@ -209,7 +212,10 @@ v2Router.get("/:name+/manifests/:reference", async (req, env: Env, context: Exec
209212
context.waitUntil(
210213
(async () => {
211214
const [response, err] = await wrap(
212-
env.REGISTRY_CLIENT.putManifest(name, reference, s2, getManifestResponse.contentType),
215+
env.REGISTRY_CLIENT.putManifest(name, reference, s2, {
216+
contentType: getManifestResponse.contentType,
217+
checkLayers: false,
218+
}),
213219
);
214220
if (err) {
215221
console.error("Error uploading asynchronously the manifest ", reference, "into main registry");
@@ -243,7 +249,7 @@ v2Router.put("/:name+/manifests/:reference", async (req, env: Env) => {
243249

244250
const { name, reference } = req.params;
245251
const [res, err] = await wrap<PutManifestResponse | RegistryError, Error>(
246-
env.REGISTRY_CLIENT.putManifest(name, reference, req.body!, req.headers.get("Content-Type")!),
252+
env.REGISTRY_CLIENT.putManifest(name, reference, req.body!, { contentType: req.headers.get("Content-Type")! }),
247253
);
248254
if (err) {
249255
console.error("Error putting manifest:", errorString(err));
@@ -563,9 +569,7 @@ v2Router.get("/:name+/tags/list", async (req, env: Env) => {
563569
cursor: tags.cursor,
564570
});
565571
// Filter out sha256 manifest
566-
manifestTags = manifestTags.concat(
567-
tags.objects.filter((tag) => !tag.key.startsWith(`${name}/manifests/sha256:`)),
568-
);
572+
manifestTags = manifestTags.concat(tags.objects.filter((tag) => !tag.key.startsWith(`${name}/manifests/sha256:`)));
569573
}
570574

571575
const keys = manifestTags.map((object) => object.key.split("/").pop()!);

0 commit comments

Comments
 (0)