build-cran-binaries/edge/rpkgs-router.test.ts
pat-s a1c1f5e78f fix(cdn): align repository routing across pull zones (#165)
## Motivation

`cran.rpkgs.com` and `cran.allianceswisspass.devxy.io` serve the same B2 repository through separate Bunny pull zones, but only the first zone was managed and purged after weekly reindexing.
This allowed the Alliance endpoint to retain stale repository metadata and left locked `renv` restores unable to retrieve versions whose binary archive object was absent.

## Changes

- Adopt the Alliance SwissPass pull zone `3265648` into OpenTofu and configure it with the shared B2 origin and middleware script.
- Purge both Bunny pull zones after the weekly rebuild reindex.
- Preserve the requested public hostname in middleware redirects.
- Redirect missing archived binaries to the corresponding CRAN source package, checking whether the version is archived or still current.
- Cover the existing archived-binary passthrough behavior in the edge routing matrix.

## Verification

- `prek run -a`
- `just edge-test`
- `crow lint .crow/`
- `tofu validate`
- `bash -n scripts/purge_cdn_zone.sh`

## Deployment

Run `tofu apply` to adopt pull zone `3265648`, publish the middleware release, and align both pull zones.
After the apply, rerun the Alliance SwissPass CI restore that requested `cli 3.6.5` and `AzureStor 3.7.1`.

Reviewed-on: #165
2026-08-13 14:08:10 +00:00

167 lines
6 KiB
TypeScript

/**
* Routing matrix for `edge/rpkgs-router.ts`.
*
* The script is exercised through the SDK's local server rather than by
* importing its internals, so what is tested is the artifact that gets
* deployed. Requests that the script passes through are proxied to the real
* origin, which keeps the "no redirect" cases honest: they assert that the
* client reached the flat slot, not merely that no `Location` was set.
*
* Run with `just edge-test`.
*/
import { assertEquals } from 'jsr:@std/assert@1';
const SCRIPT = new URL('./rpkgs-router.ts', import.meta.url).pathname;
const BASE = 'http://127.0.0.1:8080';
const UNION_SLOTS = 'amd64/alpine324';
const UA_R45_MUSL = 'R (4.5.3 x86_64-pc-linux-musl x86_64 linux-musl)';
const UA_R46_MUSL = 'R (4.6.0 x86_64-pc-linux-musl x86_64 linux-musl)';
const UA_R45_ALPINE = 'R/4.5.3 R (4.5.3 x86_64-pc-linux-musl x86_64 linux-musl) Alpine Linux 3.24';
const UA_R45_DARWIN = 'R (4.5.1 aarch64-apple-darwin20 aarch64 darwin20)';
const UA_CURL = 'curl/8.0.1';
const SLOT = '/amd64/alpine324/latest/src/contrib';
const OTHER_SLOT = '/amd64/noble/latest/src/contrib';
interface Probe {
status: number;
location: string | null;
cacheControl: string | null;
}
async function probe(path: string, userAgent: string): Promise<Probe> {
const res = await fetch(BASE + path, {
headers: { 'User-Agent': userAgent },
redirect: 'manual',
});
await res.body?.cancel();
return {
status: res.status,
location: res.headers.get('location'),
cacheControl: res.headers.get('cache-control'),
};
}
/** Kill tolerantly: the child has already exited if the script failed to load. */
async function stopServer(child: Deno.ChildProcess): Promise<void> {
try {
child.kill();
} catch {
// already gone
}
await child.status;
}
async function startServer(): Promise<Deno.ChildProcess> {
const child = new Deno.Command(Deno.execPath(), {
args: ['run', '-A', SCRIPT],
env: { UNION_SLOTS },
stdout: 'null',
stderr: 'inherit',
}).spawn();
for (let attempt = 0; attempt < 150; attempt++) {
try {
const res = await fetch(`${BASE}/`, {
headers: { 'User-Agent': UA_CURL },
redirect: 'manual',
});
await res.body?.cancel();
return child;
} catch {
await new Promise((resolve) => setTimeout(resolve, 200));
}
}
await stopServer(child);
throw new Error('edge script did not start listening on ' + BASE);
}
Deno.test('rpkgs-router', async (t) => {
const server = await startServer();
try {
await t.step("routes an index request to the client's R minor", async () => {
const res = await probe(`${SLOT}/PACKAGES.gz`, UA_R45_MUSL);
assertEquals(res.status, 302);
assertEquals(res.location, `https://cran.rpkgs.com${SLOT}/4.5/PACKAGES.gz`);
});
await t.step('routes R 4.6 to its own slot', async () => {
const res = await probe(`${SLOT}/PACKAGES.gz`, UA_R46_MUSL);
assertEquals(res.location, `https://cran.rpkgs.com${SLOT}/4.6/PACKAGES.gz`);
});
await t.step('routes PACKAGES and PACKAGES.rds too', async () => {
for (const file of ['PACKAGES', 'PACKAGES.rds']) {
const res = await probe(`${SLOT}/${file}`, UA_R45_MUSL);
assertEquals(res.location, `https://cran.rpkgs.com${SLOT}/4.5/${file}`, `expected ${file} to be routed`);
}
});
await t.step('marks the redirect uncacheable', async () => {
const res = await probe(`${SLOT}/PACKAGES.gz`, UA_R45_MUSL);
assertEquals(res.cacheControl, 'no-store');
});
await t.step('leaves a slot outside UNION_SLOTS alone', async () => {
const res = await probe(`${OTHER_SLOT}/PACKAGES.gz`, UA_R45_MUSL);
assertEquals(res.location, null);
assertEquals(res.status, 200);
});
await t.step('never routes a tarball', async () => {
const res = await probe(`${SLOT}/jsonlite_2.0.0.tar.gz`, UA_R45_MUSL);
assertEquals(res.location, null);
assertEquals(res.status, 200);
});
await t.step('serves an archived binary when it exists', async () => {
const path = `${SLOT}/Archive/xml2/xml2_1.5.2.tar.gz`;
const res = await probe(path, UA_R45_MUSL);
assertEquals(res.status, 200);
assertEquals(res.location, null);
});
await t.step('does not redirect a path already under a minor', async () => {
const res = await probe(`${SLOT}/4.5/PACKAGES.gz`, UA_R45_MUSL);
assertEquals(res.location, null);
assertEquals(res.status, 200);
});
await t.step('leaves a client without an R version alone', async () => {
const res = await probe(`${SLOT}/PACKAGES.gz`, UA_CURL);
assertEquals(res.location, null);
assertEquals(res.status, 200);
});
await t.step('resolves the bare root to slot and minor', async () => {
const res = await probe('/src/contrib/PACKAGES.gz', UA_R45_ALPINE);
assertEquals(res.location, `https://cran.rpkgs.com${SLOT}/4.5/PACKAGES.gz`);
});
await t.step('sends an unidentifiable distro to CRAN', async () => {
const res = await probe('/src/contrib/PACKAGES.gz', UA_R45_MUSL);
assertEquals(res.location, 'https://cran.r-project.org/src/contrib/PACKAGES.gz');
});
await t.step('keeps the macOS rewrite', async () => {
const res = await probe('/src/contrib/foo_1.0.tar.gz', UA_R45_DARWIN);
assertEquals(res.location, 'https://cran.rpkgs.com/bin/macosx/big-sur-arm64/contrib/4.5/foo_1.0.tar.gz');
});
await t.step('keeps the macOS binary passthrough to CRAN', async () => {
const path = '/bin/macosx/big-sur-arm64/contrib/4.5/foo_1.0.tar.gz';
const res = await probe(path, UA_R45_DARWIN);
assertEquals(res.location, `https://cran.r-project.org${path}`);
});
await t.step('collapses duplicate slashes before matching', async () => {
const res = await probe(`/amd64/alpine324//latest/src/contrib//PACKAGES.gz`, UA_R45_MUSL);
assertEquals(res.location, `https://cran.rpkgs.com${SLOT}/4.5/PACKAGES.gz`);
});
} finally {
await stopServer(server);
}
});