feat(proxies): cargo/crates pull-through cache, and fix the Go module proxy - #147
Open
luthermonson wants to merge 2 commits into
Open
feat(proxies): cargo/crates pull-through cache, and fix the Go module proxy#147luthermonson wants to merge 2 commits into
luthermonson wants to merge 2 commits into
Conversation
… proxy The Go module proxy has never once served a request. It binds to the CNI bridge gateway (10.88.0.1), but that address does not exist when the daemon starts: the bridge is created lazily by CNI on the first job container, and both cleanup() and cleanStaleBridge() delete it outright. So Listen fails with EADDRNOTAVAIL on every boot, the error is logged as a warning and swallowed, and GOPROXY is never injected into any container. Start() runs MkdirAll before the failing Listen, which is why cache/gomod exists on every node, empty, at exactly 4 KB. Three further defects would have kept it useless even with the bind fixed: EnvVars() advertised the bound address rather than the configured one; the ",direct" fallback only applies on 404/410, so a wedged proxy hard-failed builds instead of failing open; and the cleanup knob was dead code that wiped the cache on every shutdown regardless of configuration. - proxies: add Listen() with a wildcard fallback, so the gateway address becomes reachable as soon as CNI brings the bridge up. A real error (port in use) is still returned rather than masked. - go: advertise the configured address; use "|direct" so any proxy error falls through to upstream; honour cleanup=false. - cargo: new pull-through cache for the sparse index, .crate tarballs and rustup dist. The index is revalidated on a TTL with conditional GETs; tarballs are immutable and cached permanently. config.json is rewritten so dl points at the proxy while api still reaches the real registry. Fails open in three stages: not started, serve-stale, then a redirect to the origin. - runtime: support bind-mounting proxy-generated files into job containers. Cargo ignores CARGO_SOURCE_* and CARGO_REGISTRIES_* environment variables for source replacement — verified empirically, not assumed. Only a config file works, so the proxy generates .cargo/config.toml and mounts it read-only at the container root, relying on Cargo's ancestor-directory config search. That applies to any checkout path, leaves CARGO_HOME writable, and lets a repository's own config still win. No workflow changes are required. [cargo_proxy].cleanup defaults to false: wiping a pull-through cache on every restart is the bug diagnosed above. Also adds docs/arch/sccache-evaluation.md, which recommends a local-disk sccache scoped to cargo build (not docker build, to keep it disjoint from the BuildKit layer cache) with a hard size cap, sequenced after the disk-pressure GC lands.
luthermonson
marked this pull request as ready for review
August 12, 2026 00:52
Every request in cargoproxy_test.go goes through a get() helper that reads the body and closes it in a defer. bodyclose tracks the *http.Response the helper returns rather than what the helper does with it, so it reported all 30-odd call sites. The bodies are closed; restructuring the helper to avoid returning a response would churn 60 call sites to satisfy a false positive. Scoped to the test files only, matching the existing localtunnel exclusion.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Go module proxy has never served a request
It binds to the CNI bridge gateway (
10.88.0.1:8082), but that address does not exist when the daemon starts. The bridge is created lazily by CNI on the first job container, and bothcleanup()andcleanStaleBridge()delete it outright (pkg/networking/network_linux.go). SoListenfails withEADDRNOTAVAILon every boot, the error is logged as a warning and swallowed, andGOPROXYis never injected into any container.Start()runsMkdirAllbefore the failingListen, which is exactly whycache/gomodexists on every node, empty, at 4 KB. That empty directory was the clue.Three further defects would have kept it useless even with the bind fixed:
EnvVars()advertised the bound address rather than the configured one — meaningless inside a container after any fallback.GOPROXY=…,directis not fail-open: the comma separator only falls through on 404/410, so a wedged proxy hard-failed builds. Now|direct.cleanupwas dead code (if !cleanup { cleanup = true }) — the cache was wiped on every shutdown regardless of configuration.Changes
pkg/proxies/listen.go(new) —Listen()with a wildcard fallback, so the gateway address becomes reachable as soon as CNI brings the bridge up. A genuine error (port in use) is still returned rather than masked.pkg/proxies/go— advertise the configured address;|directso any proxy error falls through to upstream; honourcleanup = false.pkg/proxies/cargo(new) — pull-through cache for the sparse index,.cratetarballs and rustup dist. The index is revalidated on a TTL with conditional GETs (If-None-Match/If-Modified-Since); tarballs are immutable and cached permanently.config.jsonis rewritten sodlpoints at the proxy whileapistill reaches the real registry, socargo publish/searchkeep working. Concurrent misses are collapsed by a per-path mutex.pkg/runtime— support bind-mounting proxy-generated files into job containers.Why a mounted config file rather than environment variables
Cargo ignores
CARGO_SOURCE_*andCARGO_REGISTRIES_*for source replacement. This was verified empirically against a dead endpoint rather than assumed: with the env vars set, cargo reported "Updating crates.io index" and went straight to the real registry; the same settings in a config file produced "Updatingephemerdindex" and the expected connection error.So the proxy generates a
.cargo/config.tomland mounts it read-only at the container root, relying on Cargo's ancestor-directory config search. That applies to any checkout path, needs no knowledge of the workdir or the image'sCARGO_HOME(left writable), and a repository's own.cargo/config.tomlstill wins. No workflow changes required. rustup takesRUSTUP_DIST_SERVER, where an env var does work.Had this been built on the env-var assumption, the proxy would have been silently bypassed — the same class of failure as the Go proxy above.
Fail-open
Three stages: proxy not started → nothing injected, jobs go direct; upstream down with a cached copy → serve stale with a warning; upstream down with nothing cached → 307 to the origin. Genuine 404s pass through.
Config
[cargo_proxy]:enabled(false),port(8083),upstream(https://index.crates.io),rustup_upstream(https://static.rust-lang.org),index_ttl(10m),cleanup(false).cleanupdefaults false, deliberately unlike[module_proxy]— wiping a pull-through cache on every restart is the bug diagnosed above. Registered inmanagedCaches()asLiveSafe: true; the generated container config lives outside the cache root socache clear cargocannot pull a mounted file out from under a running job.Testing
go build ./...andgo vet ./...clean;go test ./...passes apart from a pre-existingpkg/runnerfailure (fresh worktree lacks the gitignored runner archive). ~45 tests against anhttptestfake upstream, no network access.Not yet run on a node — opened as a draft. The Go-proxy fix in particular is worth confirming live, since the symptom (an empty 4 KB directory) is easy to mistake for "working but unused".
Follow-ups found, not fixed
Manager.GatewayIP()hardcodes10.88.0.1whenSubnetis empty, whileConfig.subnet()→pickSubnet()may auto-select a different10.x/16on conflict. On such a host the two disagree.GatewayPortsACCEPT rules land inEPHEMERD-FORWARD, jumped fromFORWARD, but container→gateway traffic hitsINPUT— so those rules are currently no-ops. Harmless today (reachability comes from the default INPUT policy), but the carve-out is not doing what it appears to.sccache
docs/arch/sccache-evaluation.md. Recommends building it, sequenced after the disk-pressure GC, with a hard size cap wired into that GC from day one. Local-disk backend, partitioned per repo, and scoped tocargo buildrather thandocker buildso it stays disjoint from the BuildKit layer cache instead of storing the same output twice.