Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 71 additions & 1 deletion src/registry/r2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ export async function encodeState(state: State, env: Env): Promise<{ jwt: string
}

export const symlinkHeader = "X-Serverless-Registry-Symlink";
export const symlinkDigestHeader = "X-Serverless-Registry-Symlink-Digest";
export const symlinkSizeHeader = "X-Serverless-Registry-Symlink-Size";

export async function getUploadState(
name: string,
Expand Down Expand Up @@ -417,11 +419,22 @@ export class R2Registry implements Registry {
// Trying to mount a layer from sourceLayerPath to destinationLayerPath

// Create linked file with custom metadata
if (res.checksums.sha256 === null) {
return { response: new ServerError("invalid checksum from R2 backend") };
}
const [newFile, error] = await wrap(
this.env.REGISTRY.put(destinationLayerPath, sourceLayerPath, {
// Symlink object content is the source blob path string.
// The object checksum must match the symlink payload to satisfy R2.
sha256: await getSHA256(sourceLayerPath, ""),
httpMetadata: res.httpMetadata,
customMetadata: { [symlinkHeader]: sourceName }, // Storing target repository name in metadata (to easily resolve recursive layer mounting)
customMetadata: {
// Storing target repository name in metadata (to easily resolve recursive layer mounting)
[symlinkHeader]: sourceName,
// Store source layer metadata so HEAD can answer without loading symlink body.
[symlinkDigestHeader]: digest,
[symlinkSizeHeader]: `${res.size}`,
},
}),
);
if (error) {
Expand Down Expand Up @@ -450,6 +463,63 @@ export class R2Registry implements Registry {
};
}

const expectedDigest =
tag.startsWith("sha256:") && tag.length > SHA256_PREFIX_LEN ? tag.slice(SHA256_PREFIX_LEN) : null;
const symlinkByChecksumMismatch =
expectedDigest !== null && res.checksums.sha256 !== null && res.checksums.sha256 !== expectedDigest;
const symlinkMetadata = res.customMetadata ?? {};
const symlinkByMetadata = symlinkHeader in symlinkMetadata;

// Handle R2 symlink layers.
// We detect symlinks by:
// 1) explicit metadata, or
// 2) checksum mismatch between requested digest and R2 object checksum
// (the symlink object checksum is based on symlink payload, not mounted blob bytes).
if (symlinkByMetadata || symlinkByChecksumMismatch) {
// Fast path for symlinks created by newer versions that include source metadata.
const metadataSize = +(symlinkMetadata[symlinkSizeHeader] ?? "");
const metadataDigest = symlinkMetadata[symlinkDigestHeader];
if (Number.isFinite(metadataSize) && metadataSize >= 0 && metadataDigest) {
return {
digest: metadataDigest,
size: metadataSize,
exists: true,
};
}

// Backward-compatibility path for old symlinks: resolve link body and query target.
const [obj, getErr] = await wrap(this.env.REGISTRY.get(`${name}/blobs/${tag}`));
if (getErr) {
return wrapError("layerExists", getErr);
}
if (!obj) {
return { exists: false };
}

const layerPath = await obj.text();
const [linkName, linkDigest] = layerPath.split("/blobs/");
if (!linkName || !linkDigest) {
// Backward compatibility: if this does not look like a symlink payload,
// fall back to object metadata from HEAD.
if (res.checksums.sha256 === null) {
return { response: new ServerError("invalid checksum from R2 backend") };
}

return {
digest: hexToDigest(res.checksums.sha256!),
size: res.size,
exists: true,
};
}

// Prevent recursive self-reference.
if (linkName === name && linkDigest === tag) {
return { exists: false };
}

return await this.env.REGISTRY_CLIENT.layerExists(linkName, linkDigest);
}

return {
digest: hexToDigest(res.checksums.sha256!),
size: res.size,
Expand Down
17 changes: 6 additions & 11 deletions src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,12 @@ v2Router.put("/:name+/blobs/uploads/:uuid", async (req, env: Env) => {
v2Router.head("/:name+/blobs/:tag", async (req, env: Env) => {
const { name, tag } = req.params;

const res = await env.REGISTRY.head(`${name}/blobs/${tag}`);
let layerExistsResponse: CheckLayerResponse | null = null;
if (!res) {
const localResponse = await env.REGISTRY_CLIENT.layerExists(name, tag);
if ("response" in localResponse) {
return localResponse.response;
}
if (!localResponse.exists) {
const registryList = registries(env);
for (const registry of registryList) {
const client = new RegistryHTTPClient(env, registry);
Expand All @@ -521,15 +524,7 @@ v2Router.head("/:name+/blobs/:tag", async (req, env: Env) => {
if (layerExistsResponse === null || !layerExistsResponse.exists)
return new Response(JSON.stringify(BlobUnknownError), { status: 404 });
} else {
if (res.checksums.sha256 === null) {
throw new ServerError("invalid checksum from R2 backend");
}

layerExistsResponse = {
digest: hexToDigest(res.checksums.sha256!),
size: res.size,
exists: true,
};
layerExistsResponse = localResponse;
}

return new Response(null, {
Expand Down
11 changes: 11 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,17 @@ describe("v2 manifests", () => {
expect(layerC.ok).toBeTruthy();
expect(await layerB.bytes()).toEqual(sourceData);
expect(await layerC.bytes()).toEqual(sourceData);

// Check layer HEAD returns source metadata for mounted symlinks.
const layerHeadB = await fetch(createRequest("HEAD", `/v2/${repoB}/blobs/${layer}`, null));
expect(layerHeadB.ok).toBeTruthy();
expect(layerHeadB.headers.get("Docker-Content-Digest")).toEqual(layer);
expect(+(layerHeadB.headers.get("Content-Length") ?? "-1")).toEqual(sourceData.byteLength);

const layerHeadC = await fetch(createRequest("HEAD", `/v2/${repoC}/blobs/${layer}`, null));
expect(layerHeadC.ok).toBeTruthy();
expect(layerHeadC.headers.get("Docker-Content-Digest")).toEqual(layer);
expect(+(layerHeadC.headers.get("Content-Length") ?? "-1")).toEqual(sourceData.byteLength);
}
}
});
Expand Down