Skip to content

Commit a44b647

Browse files
cberesculordnimrod
andauthored
http client: add docker.io fallback support and default namespace (#74)
* Update http.ts * add default namespace * Update http.ts * added prettier and also fixed let to const * moved library default to http.ts * resolved unused and out of bounds variables --------- Co-authored-by: Ciprian <[email protected]>
1 parent 3e532ba commit a44b647

10 files changed

+1187
-873
lines changed

.prettierrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module.exports = {
22
singleQuote: false,
3-
trailingComma: 'all',
3+
trailingComma: "all",
44
tabWidth: 2,
55
printWidth: 120,
66
semi: true,

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "r2-registry",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "An open-source R2 registry",
55
"main": "index.ts",
66
"scripts": {
@@ -21,6 +21,7 @@
2121
"cross-env": "^7.0.3",
2222
"eslint": "^8.57.0",
2323
"miniflare": "3.20240909.4",
24+
"prettier": "3.3.3",
2425
"typescript": "^5.3.3",
2526
"vitest": "^2.1.0",
2627
"wrangler": "^3.78.7"

pnpm-lock.yaml

Lines changed: 1142 additions & 843 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

push/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ if (!(await file(tarFile).exists())) {
7777
});
7878

7979
console.log(`Extracted to ${imagePath}`);
80-
}
80+
}
8181

8282
type DockerSaveConfigManifest = {
8383
Config: string;

src/authentication-method.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export async function authenticationMethodFromEnv(env: Env) {
1919
return new UserAuthenticator(credentials);
2020
}
2121

22-
console.error("Either env.JWT_REGISTRY_TOKENS_PUBLIC_KEY must be set or both env.USERNAME, env.PASSWORD must be set or both env.READONLY_USERNAME, env.READONLY_PASSWORD must be set.");
22+
console.error(
23+
"Either env.JWT_REGISTRY_TOKENS_PUBLIC_KEY must be set or both env.USERNAME, env.PASSWORD must be set or both env.READONLY_USERNAME, env.READONLY_PASSWORD must be set.",
24+
);
2325

2426
// invalid configuration
2527
return undefined;

src/registry/http.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ function ctxIntoHeaders(ctx: HTTPContext): Headers {
6363
}
6464

6565
function ctxIntoRequest(ctx: HTTPContext, url: URL, method: string, path: string, body?: BodyInit): Request {
66-
const urlReq = `${url.protocol}//${ctx.authContext.service}/v2${
67-
ctx.repository === "" ? "/" : ctx.repository + "/"
66+
const urlReq = `${url.protocol}//${url.host}/v2${
67+
ctx.repository === "" || ctx.repository === "/" ? "/" : ctx.repository + "/"
6868
}${path}`;
6969
return new Request(urlReq, {
7070
method,
@@ -141,7 +141,10 @@ function authHeaderIntoAuthContext(urlObject: URL, authenticateHeader: string):
141141
export class RegistryHTTPClient implements Registry {
142142
private url: URL;
143143

144-
constructor(private env: Env, private configuration: RegistryConfiguration) {
144+
constructor(
145+
private env: Env,
146+
private configuration: RegistryConfiguration,
147+
) {
145148
this.url = new URL(configuration.registry);
146149
}
147150

@@ -153,7 +156,7 @@ export class RegistryHTTPClient implements Registry {
153156
return (this.env as unknown as Record<string, string>)[this.configuration.password_env] ?? "";
154157
}
155158

156-
async authenticate(): Promise<HTTPContext> {
159+
async authenticate(namespace: string): Promise<HTTPContext> {
157160
const res = await fetch(`${this.url.protocol}//${this.url.host}/v2/`, {
158161
headers: {
159162
"User-Agent": "Docker-Client/24.0.5 (linux)",
@@ -185,6 +188,7 @@ export class RegistryHTTPClient implements Registry {
185188
}
186189

187190
const authCtx = authHeaderIntoAuthContext(this.url, authenticateHeader);
191+
if (!authCtx.scope) authCtx.scope = namespace;
188192
switch (authCtx.authType) {
189193
case "bearer":
190194
return await this.authenticateBearer(authCtx);
@@ -221,7 +225,7 @@ export class RegistryHTTPClient implements Registry {
221225
const params = new URLSearchParams({
222226
service: ctx.service,
223227
// explicitely include that we don't want an offline_token.
224-
scope: `repository:${this.url.pathname.slice(1)}/image:pull,push`,
228+
scope: `repository:${ctx.scope}:pull,push`,
225229
client_id: "r2registry",
226230
grant_type: "password",
227231
password: this.password(),
@@ -261,7 +265,6 @@ export class RegistryHTTPClient implements Registry {
261265
repository: string;
262266
token?: string;
263267
} = JSON.parse(t);
264-
265268
console.debug(
266269
`Authenticated with registry ${this.url.toString()} successfully, got token that expires in ${
267270
response.expires_in
@@ -309,9 +312,10 @@ export class RegistryHTTPClient implements Registry {
309312
};
310313
}
311314

312-
async manifestExists(namespace: string, tag: string): Promise<CheckManifestResponse | RegistryError> {
315+
async manifestExists(name: string, tag: string): Promise<CheckManifestResponse | RegistryError> {
316+
const namespace = name.includes("/") ? name : `library/${name}`;
313317
try {
314-
const ctx = await this.authenticate();
318+
const ctx = await this.authenticate(namespace);
315319
const req = ctxIntoRequest(ctx, this.url, "HEAD", `${namespace}/manifests/${tag}`);
316320
req.headers.append("Accept", manifestTypes.join(", "));
317321
const res = await fetch(req);
@@ -337,9 +341,10 @@ export class RegistryHTTPClient implements Registry {
337341
}
338342
}
339343

340-
async getManifest(namespace: string, digest: string): Promise<GetManifestResponse | RegistryError> {
344+
async getManifest(name: string, digest: string): Promise<GetManifestResponse | RegistryError> {
345+
const namespace = name.includes("/") ? name : `library/${name}`;
341346
try {
342-
const ctx = await this.authenticate();
347+
const ctx = await this.authenticate(namespace);
343348
const req = ctxIntoRequest(ctx, this.url, "GET", `${namespace}/manifests/${digest}`);
344349
req.headers.append("Accept", manifestTypes.join(", "));
345350
const res = await fetch(req);
@@ -368,9 +373,10 @@ export class RegistryHTTPClient implements Registry {
368373
}
369374
}
370375

371-
async layerExists(namespace: string, digest: string): Promise<CheckLayerResponse | RegistryError> {
376+
async layerExists(name: string, digest: string): Promise<CheckLayerResponse | RegistryError> {
377+
const namespace = name.includes("/") ? name : `library/${name}`;
372378
try {
373-
const ctx = await this.authenticate();
379+
const ctx = await this.authenticate(namespace);
374380
const res = await fetch(ctxIntoRequest(ctx, this.url, "HEAD", `${namespace}/blobs/${digest}`));
375381
if (res.status === 404) {
376382
return {
@@ -407,9 +413,10 @@ export class RegistryHTTPClient implements Registry {
407413
}
408414
}
409415

410-
async getLayer(namespace: string, digest: string): Promise<GetLayerResponse | RegistryError> {
416+
async getLayer(name: string, digest: string): Promise<GetLayerResponse | RegistryError> {
417+
const namespace = name.includes("/") ? name : `library/${name}`;
411418
try {
412-
const ctx = await this.authenticate();
419+
const ctx = await this.authenticate(namespace);
413420
const req = ctxIntoRequest(ctx, this.url, "GET", `${namespace}/blobs/${digest}`);
414421
let res = await fetch(req);
415422
if (!res.ok) {

src/router.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,8 +327,9 @@ v2Router.delete("/:name+/blobs/uploads/:id", async (req, env: Env) => {
327327

328328
// this is the first thing that the client asks for in an upload
329329
v2Router.post("/:name+/blobs/uploads/", async (req, env: Env) => {
330-
const { name } = req.params;
330+
const { name } = req.params;
331331
const [uploadObject, err] = await wrap<UploadObject | RegistryError, Error>(env.REGISTRY_CLIENT.startUpload(name));
332+
332333
if (err) {
333334
return new InternalError();
334335
}
@@ -361,6 +362,7 @@ v2Router.get("/:name+/blobs/uploads/:uuid", async (req, env: Env) => {
361362
const [uploadObject, err] = await wrap<UploadObject | RegistryError, Error>(
362363
env.REGISTRY_CLIENT.getUpload(name, uuid),
363364
);
365+
364366
if (err) {
365367
return new InternalError();
366368
}
@@ -389,6 +391,7 @@ v2Router.patch("/:name+/blobs/uploads/:uuid", async (req, env: Env) => {
389391
const { name, uuid } = req.params;
390392
const contentRange = req.headers.get("Content-Range");
391393
const [start, end] = contentRange?.split("-") ?? [undefined, undefined];
394+
392395
if (req.body == null) {
393396
return new Response(null, { status: 400 });
394397
}
@@ -516,6 +519,7 @@ export type TagsList = {
516519

517520
v2Router.get("/:name+/tags/list", async (req, env: Env) => {
518521
const { name } = req.params;
522+
519523
const { n: nStr = 50, last } = req.query;
520524
const n = +nStr;
521525
if (isNaN(n)) {
@@ -564,6 +568,7 @@ v2Router.delete("/:name+/blobs/:digest", async (req, env: Env) => {
564568

565569
v2Router.post("/:name+/gc", async (req, env: Env) => {
566570
const { name } = req.params;
571+
567572
const mode = req.query.mode ?? "unreferenced";
568573
if (mode !== "unreferenced" && mode !== "untagged") {
569574
throw new ServerError("Mode must be either 'unreferenced' or 'untagged'", 400);

src/v2-errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const ManifestUnknownError = (tag: string) =>
99
},
1010
},
1111
],
12-
} as const);
12+
}) as const;
1313

1414
export const BlobUnknownError = {
1515
errors: [

test/tsconfig.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
{
22
"include": ["vitest.config.ts"],
33
"compilerOptions": {
4-
"strict": true,
5-
"module": "esnext",
6-
"target": "esnext",
7-
"lib": ["esnext"],
8-
"moduleResolution": "bundler",
9-
"noEmit": true,
10-
"skipLibCheck": true,
11-
"allowSyntheticDefaultImports": true
12-
}
4+
"strict": true,
5+
"module": "esnext",
6+
"target": "esnext",
7+
"lib": ["esnext"],
8+
"moduleResolution": "bundler",
9+
"noEmit": true,
10+
"skipLibCheck": true,
11+
"allowSyntheticDefaultImports": true
12+
}
1313
}

tsconfig.base.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"experimentalDecorators": true,
66
"module": "esnext",
77
"moduleResolution": "node",
8-
"types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers"],
8+
"types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers"],
99
"resolveJsonModule": true,
1010
"allowJs": true,
1111
"noEmit": true,

0 commit comments

Comments
 (0)