From 41035a1c4f05ee432b26f6f2761a2fafb37b6375 Mon Sep 17 00:00:00 2001 From: unkin-agent Date: Sat, 17 Oct 2026 15:42:17 +1100 Subject: [PATCH] add common caching doc --- README.md | 3 ++ docs/alpine/remote-github.md | 2 + docs/alpine/remote.md | 2 + docs/cargo/remote.md | 2 + docs/common/caching.md | 100 +++++++++++++++++++++++++++++++++++ docs/deb/remote-github.md | 2 + docs/deb/remote.md | 2 + docs/docker/remote.md | 2 + docs/generic/remote.md | 2 + docs/goproxy/remote.md | 2 + docs/helm/remote.md | 2 + docs/npm/remote.md | 2 + docs/nuget/remote.md | 2 + docs/puppet/remote.md | 2 + docs/pypi/remote.md | 2 + docs/rpm/remote-github.md | 2 + docs/rpm/remote.md | 2 + docs/terraform/remote.md | 2 + 18 files changed, 135 insertions(+) create mode 100644 docs/common/caching.md diff --git a/README.md b/README.md index 5ae9df8..4f0a78c 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,9 @@ API: `http://localhost:8000` | Frontend: `http://localhost:5173` ## Package Types Providers classify paths automatically. Users only configure what to proxy and TTLs. + +Classification precedence, TTLs and eviction for every remote: [docs/common/caching.md](docs/common/caching.md). + Each type's paths, Terraform examples and local/virtual behaviour are in `docs//`: - `alpine`: [remote](docs/alpine/remote.md), [remote-github](docs/alpine/remote-github.md) diff --git a/docs/alpine/remote-github.md b/docs/alpine/remote-github.md index 2342ee1..e9e473b 100644 --- a/docs/alpine/remote-github.md +++ b/docs/alpine/remote-github.md @@ -1,5 +1,7 @@ # `github_alpine` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `github_alpine` | `APKINDEX.tar.gz` (synthesized) | `.apk` (redirected) | diff --git a/docs/alpine/remote.md b/docs/alpine/remote.md index 8a3761b..f415b81 100644 --- a/docs/alpine/remote.md +++ b/docs/alpine/remote.md @@ -1,5 +1,7 @@ # `alpine` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `alpine` | `APKINDEX.tar.gz` | `.apk` | diff --git a/docs/cargo/remote.md b/docs/cargo/remote.md index bbbb8ee..d7bec9e 100644 --- a/docs/cargo/remote.md +++ b/docs/cargo/remote.md @@ -1,5 +1,7 @@ # `cargo` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `cargo` | sparse index files (`config.json` synthesized) | `crates/*/*.crate` | diff --git a/docs/common/caching.md b/docs/common/caching.md new file mode 100644 index 0000000..82da94b --- /dev/null +++ b/docs/common/caching.md @@ -0,0 +1,100 @@ +# Caching and classification (all remotes) + +Every proxied request on a remote is classified as **denied**, **immutable** or +**mutable**. The class picks the storage path and the freshness TTL. + +## Fields + +| Field | Meaning | +|---|---| +| `patterns` | Allowlist of regexes. Empty allows every path; otherwise a non-matching path is denied (403). | +| `blocklist` | Regexes; a match is denied (403). | +| `immutable_patterns` | Regexes forcing a path immutable. | +| `mutable_patterns` | Regexes forcing a path mutable. | +| `immutable_ttl` | Seconds an immutable path stays fresh. `0` = forever. | +| `mutable_ttl` | Seconds a mutable path stays fresh. `0` also means never expires, and the API stores `0` when the field is omitted. | +| `check_mutable` | Once a mutable path expires, revalidate with a conditional `HEAD` (`If-None-Match`) before refetching. | +| `stale_on_error` | On an upstream network error, serve the stored copy instead of failing. | +| `quarantine_enabled` / `quarantine_days` | Stored on the remote but not enforced by the proxy yet. | +| `mirrorlist` / `mirror_strategy` | rpm/deb/alpine only. Upstream pool is `[base_url] + mirrorlist`; `round_robin` (default) rotates the starting mirror, `least_conn` starts at the one with the fewest in-flight requests. Network errors and 5xx fail over to the next mirror; other statuses return as-is. | + +## Precedence + +The first rule that matches wins: + +1. `blocklist` matches: **denied** +2. `patterns` is non-empty and nothing matches: **denied** +3. `immutable_patterns` matches: **immutable** +4. `mutable_patterns` matches: **mutable** +5. the provider's built-in `Classify` says mutable: **mutable** +6. anything else: **immutable** + +So `blocklist` beats the allowlist, and `immutable_patterns` beats +`mutable_patterns`. Either user list overrides the built-in rules. `GET` and +`HEAD` both apply this order. + +## Matching rules + +- Regexes are Go `regexp`, matched **unanchored** (`MatchString`). Add `^` or `$` yourself when you need them. +- They match the path after `/api/v1/remote//`, without a leading slash, e.g. `9/BaseOS/x86_64/os/repodata/repomd.xml`. +- The query string is not part of the path. It is dropped and not forwarded upstream. +- An invalid regex is silently skipped. + +## Lifecycles + +| | Immutable | Mutable | +|---|---|---| +| Stored as | content-addressed blob (sha256), streamed | per-path index object, buffered and rewritten by the provider | +| Fresh for | `immutable_ttl` (`0` = forever) | `mutable_ttl` | +| After expiry | refetched from upstream | with `check_mutable` and a stored ETag, upstream `304` re-arms the TTL and serves the cached copy; otherwise refetched | +| Upstream down | `stale_on_error` serves the stored copy and re-arms the TTL; an open circuit breaker always serves stale if a copy exists, else 503 | same | + +Concurrent misses on one path share a fetch lock. Waiters poll the store for +up to 5s instead of all going upstream. + +## Eviction + +```sh +curl -X DELETE https://artifactapi.example.com/api/v2/remotes//objects/ +curl -X DELETE https://artifactapi.example.com/api/v2/remotes//objects//* +``` + +- Evict drops the artifact row, the index object and the Redis TTL and ETag keys, so the next request goes upstream whatever the class or TTL. +- A single-path evict takes the fetch lock first, so an in-flight fetch can't re-cache the path. It returns 503 if the lock stays busy. +- A wildcard must end in `/*` and takes no lock. +- Returns 204, or 404 for an unknown remote. + +## Examples + +Bodies for `POST https://artifactapi.example.com/api/v2/remotes`. + +GitHub release assets through a `generic` remote, allowlisted: + +```json +{"name": "github", "package_type": "generic", "base_url": "https://github.com", + "immutable_ttl": 0, "mutable_ttl": 7200, + "patterns": ["^mikefarah/yq/releases/download/.*/yq_linux_amd64$"]} +``` + +rpm remote: `repodata/*` and `repomd.xml` are mutable by default, so only the TTLs are needed: + +```json +{"name": "almalinux", "package_type": "rpm", "base_url": "https://repo.almalinux.org/almalinux", + "immutable_ttl": 0, "mutable_ttl": 3600, "check_mutable": true, "stale_on_error": true} +``` + +Force a moving path mutable, e.g. branch tarballs on `generic`: + +```json +{"mutable_patterns": ["/archive/refs/heads/.*\\.tar\\.gz$"], "mutable_ttl": 600} +``` + +## Built-in classification per type + +Each type's page lists its auto-detected mutable and immutable paths: +[alpine](../alpine/remote.md), [alpine github](../alpine/remote-github.md), +[cargo](../cargo/remote.md), [deb](../deb/remote.md), [deb github](../deb/remote-github.md), +[docker](../docker/remote.md), [generic](../generic/remote.md), [goproxy](../goproxy/remote.md), +[helm](../helm/remote.md), [npm](../npm/remote.md), [nuget](../nuget/remote.md), +[puppet](../puppet/remote.md), [pypi](../pypi/remote.md), [rpm](../rpm/remote.md), +[rpm github](../rpm/remote-github.md), [terraform](../terraform/remote.md). diff --git a/docs/deb/remote-github.md b/docs/deb/remote-github.md index a6072ba..99d9f11 100644 --- a/docs/deb/remote-github.md +++ b/docs/deb/remote-github.md @@ -1,5 +1,7 @@ # `github_deb` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `github_deb` | `Packages*`, `Release*` (synthesized) | `.deb` (redirected) | diff --git a/docs/deb/remote.md b/docs/deb/remote.md index b8ec425..865d949 100644 --- a/docs/deb/remote.md +++ b/docs/deb/remote.md @@ -1,5 +1,7 @@ # `deb` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `deb` | `Packages*`, `Sources*`, `Release`, `InRelease`, `Release.gpg`, `Contents-*` | `.deb` | diff --git a/docs/docker/remote.md b/docs/docker/remote.md index bdff0fb..c8a79f1 100644 --- a/docs/docker/remote.md +++ b/docs/docker/remote.md @@ -1,5 +1,7 @@ # `docker` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `docker` | tag manifests, `/tags/list` | blobs, digest manifests | diff --git a/docs/generic/remote.md b/docs/generic/remote.md index 38473d7..7fac792 100644 --- a/docs/generic/remote.md +++ b/docs/generic/remote.md @@ -1,5 +1,7 @@ # `generic` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `generic` | nothing | everything | diff --git a/docs/goproxy/remote.md b/docs/goproxy/remote.md index 69fcd36..52d1c49 100644 --- a/docs/goproxy/remote.md +++ b/docs/goproxy/remote.md @@ -1,5 +1,7 @@ # `goproxy` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `goproxy` | `@v/list`, `@latest` | `.info`, `.mod`, `.zip` | diff --git a/docs/helm/remote.md b/docs/helm/remote.md index 617c525..b51e028 100644 --- a/docs/helm/remote.md +++ b/docs/helm/remote.md @@ -1,5 +1,7 @@ # `helm` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `helm` | `index.yaml` | `.tgz` charts | diff --git a/docs/npm/remote.md b/docs/npm/remote.md index 21fb645..8559315 100644 --- a/docs/npm/remote.md +++ b/docs/npm/remote.md @@ -1,5 +1,7 @@ # `npm` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `npm` | package metadata | `.tgz` tarballs | diff --git a/docs/nuget/remote.md b/docs/nuget/remote.md index d4cb308..ba0109a 100644 --- a/docs/nuget/remote.md +++ b/docs/nuget/remote.md @@ -1,5 +1,7 @@ # `nuget` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `nuget` | `index.json` (service index), registration and flat container indexes | `.nupkg`, `.nuspec` | diff --git a/docs/puppet/remote.md b/docs/puppet/remote.md index f0cbc7f..8c7435e 100644 --- a/docs/puppet/remote.md +++ b/docs/puppet/remote.md @@ -1,5 +1,7 @@ # `puppet` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `puppet` | `v3/modules/*`, `v3/releases*` | `.tar.gz` | diff --git a/docs/pypi/remote.md b/docs/pypi/remote.md index a4660e6..0f5fe32 100644 --- a/docs/pypi/remote.md +++ b/docs/pypi/remote.md @@ -1,5 +1,7 @@ # `pypi` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `pypi` | `simple/*` index pages | `.whl`, `.tar.gz` | diff --git a/docs/rpm/remote-github.md b/docs/rpm/remote-github.md index 2426a44..f079b02 100644 --- a/docs/rpm/remote-github.md +++ b/docs/rpm/remote-github.md @@ -1,5 +1,7 @@ # `github_rpm` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `github_rpm` | `repodata/*` (synthesized) | `.rpm` (redirected) | diff --git a/docs/rpm/remote.md b/docs/rpm/remote.md index db85dac..f4a93b2 100644 --- a/docs/rpm/remote.md +++ b/docs/rpm/remote.md @@ -1,5 +1,7 @@ # `rpm` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `rpm` | `repomd.xml`, `repodata/*` | `.rpm` | diff --git a/docs/terraform/remote.md b/docs/terraform/remote.md index a8db829..d4fe58b 100644 --- a/docs/terraform/remote.md +++ b/docs/terraform/remote.md @@ -1,5 +1,7 @@ # `terraform` remote +Shared classification, TTL and eviction rules: [caching](../common/caching.md). + | Type | Mutable (auto-detected) | Immutable (auto-detected) | |---|---|---| | `terraform` | `*/versions` | `*/download/*/*` |