Skip to content

Commit

Permalink
http client: add docker.io fallback support and default namespace (#74)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
cberescu and lordnimrod authored Nov 20, 2024
1 parent 3e532ba commit a44b647
Show file tree
Hide file tree
Showing 10 changed files with 1,187 additions and 873 deletions.
2 changes: 1 addition & 1 deletion .prettierrc.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module.exports = {
singleQuote: false,
trailingComma: 'all',
trailingComma: "all",
tabWidth: 2,
printWidth: 120,
semi: true,
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "r2-registry",
"version": "1.0.0",
"version": "1.1.0",
"description": "An open-source R2 registry",
"main": "index.ts",
"scripts": {
Expand All @@ -21,6 +21,7 @@
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"miniflare": "3.20240909.4",
"prettier": "3.3.3",
"typescript": "^5.3.3",
"vitest": "^2.1.0",
"wrangler": "^3.78.7"
Expand Down
1,985 changes: 1,142 additions & 843 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion push/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ if (!(await file(tarFile).exists())) {
});

console.log(`Extracted to ${imagePath}`);
}
}

type DockerSaveConfigManifest = {
Config: string;
Expand Down
4 changes: 3 additions & 1 deletion src/authentication-method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ export async function authenticationMethodFromEnv(env: Env) {
return new UserAuthenticator(credentials);
}

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.");
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.",
);

// invalid configuration
return undefined;
Expand Down
35 changes: 21 additions & 14 deletions src/registry/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ function ctxIntoHeaders(ctx: HTTPContext): Headers {
}

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

constructor(private env: Env, private configuration: RegistryConfiguration) {
constructor(
private env: Env,
private configuration: RegistryConfiguration,
) {
this.url = new URL(configuration.registry);
}

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

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

const authCtx = authHeaderIntoAuthContext(this.url, authenticateHeader);
if (!authCtx.scope) authCtx.scope = namespace;
switch (authCtx.authType) {
case "bearer":
return await this.authenticateBearer(authCtx);
Expand Down Expand Up @@ -221,7 +225,7 @@ export class RegistryHTTPClient implements Registry {
const params = new URLSearchParams({
service: ctx.service,
// explicitely include that we don't want an offline_token.
scope: `repository:${this.url.pathname.slice(1)}/image:pull,push`,
scope: `repository:${ctx.scope}:pull,push`,
client_id: "r2registry",
grant_type: "password",
password: this.password(),
Expand Down Expand Up @@ -261,7 +265,6 @@ export class RegistryHTTPClient implements Registry {
repository: string;
token?: string;
} = JSON.parse(t);

console.debug(
`Authenticated with registry ${this.url.toString()} successfully, got token that expires in ${
response.expires_in
Expand Down Expand Up @@ -309,9 +312,10 @@ export class RegistryHTTPClient implements Registry {
};
}

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

async getManifest(namespace: string, digest: string): Promise<GetManifestResponse | RegistryError> {
async getManifest(name: string, digest: string): Promise<GetManifestResponse | RegistryError> {
const namespace = name.includes("/") ? name : `library/${name}`;
try {
const ctx = await this.authenticate();
const ctx = await this.authenticate(namespace);
const req = ctxIntoRequest(ctx, this.url, "GET", `${namespace}/manifests/${digest}`);
req.headers.append("Accept", manifestTypes.join(", "));
const res = await fetch(req);
Expand Down Expand Up @@ -368,9 +373,10 @@ export class RegistryHTTPClient implements Registry {
}
}

async layerExists(namespace: string, digest: string): Promise<CheckLayerResponse | RegistryError> {
async layerExists(name: string, digest: string): Promise<CheckLayerResponse | RegistryError> {
const namespace = name.includes("/") ? name : `library/${name}`;
try {
const ctx = await this.authenticate();
const ctx = await this.authenticate(namespace);
const res = await fetch(ctxIntoRequest(ctx, this.url, "HEAD", `${namespace}/blobs/${digest}`));
if (res.status === 404) {
return {
Expand Down Expand Up @@ -407,9 +413,10 @@ export class RegistryHTTPClient implements Registry {
}
}

async getLayer(namespace: string, digest: string): Promise<GetLayerResponse | RegistryError> {
async getLayer(name: string, digest: string): Promise<GetLayerResponse | RegistryError> {
const namespace = name.includes("/") ? name : `library/${name}`;
try {
const ctx = await this.authenticate();
const ctx = await this.authenticate(namespace);
const req = ctxIntoRequest(ctx, this.url, "GET", `${namespace}/blobs/${digest}`);
let res = await fetch(req);
if (!res.ok) {
Expand Down
7 changes: 6 additions & 1 deletion src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,9 @@ v2Router.delete("/:name+/blobs/uploads/:id", async (req, env: Env) => {

// this is the first thing that the client asks for in an upload
v2Router.post("/:name+/blobs/uploads/", async (req, env: Env) => {
const { name } = req.params;
const { name } = req.params;
const [uploadObject, err] = await wrap<UploadObject | RegistryError, Error>(env.REGISTRY_CLIENT.startUpload(name));

if (err) {
return new InternalError();
}
Expand Down Expand Up @@ -361,6 +362,7 @@ v2Router.get("/:name+/blobs/uploads/:uuid", async (req, env: Env) => {
const [uploadObject, err] = await wrap<UploadObject | RegistryError, Error>(
env.REGISTRY_CLIENT.getUpload(name, uuid),
);

if (err) {
return new InternalError();
}
Expand Down Expand Up @@ -389,6 +391,7 @@ v2Router.patch("/:name+/blobs/uploads/:uuid", async (req, env: Env) => {
const { name, uuid } = req.params;
const contentRange = req.headers.get("Content-Range");
const [start, end] = contentRange?.split("-") ?? [undefined, undefined];

if (req.body == null) {
return new Response(null, { status: 400 });
}
Expand Down Expand Up @@ -516,6 +519,7 @@ export type TagsList = {

v2Router.get("/:name+/tags/list", async (req, env: Env) => {
const { name } = req.params;

const { n: nStr = 50, last } = req.query;
const n = +nStr;
if (isNaN(n)) {
Expand Down Expand Up @@ -564,6 +568,7 @@ v2Router.delete("/:name+/blobs/:digest", async (req, env: Env) => {

v2Router.post("/:name+/gc", async (req, env: Env) => {
const { name } = req.params;

const mode = req.query.mode ?? "unreferenced";
if (mode !== "unreferenced" && mode !== "untagged") {
throw new ServerError("Mode must be either 'unreferenced' or 'untagged'", 400);
Expand Down
2 changes: 1 addition & 1 deletion src/v2-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const ManifestUnknownError = (tag: string) =>
},
},
],
} as const);
}) as const;

export const BlobUnknownError = {
errors: [
Expand Down
18 changes: 9 additions & 9 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"include": ["vitest.config.ts"],
"compilerOptions": {
"strict": true,
"module": "esnext",
"target": "esnext",
"lib": ["esnext"],
"moduleResolution": "bundler",
"noEmit": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true
}
"strict": true,
"module": "esnext",
"target": "esnext",
"lib": ["esnext"],
"moduleResolution": "bundler",
"noEmit": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true
}
}
2 changes: 1 addition & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers"],
"types": ["@cloudflare/workers-types", "@cloudflare/vitest-pool-workers"],
"resolveJsonModule": true,
"allowJs": true,
"noEmit": true,
Expand Down

0 comments on commit a44b647

Please sign in to comment.