Add an offline mode: serve only the cache, contact no upstream
Part of #54 (closed) (milestone 1). Snap install/refresh while offline is the follow-up.
-offline (and GET/PUT /v1/offline on the admin socket, to flip it at run time) makes pkgproxy really offline:
- Nothing goes upstream. Every side fetches through
cmd.upstreamTransport()andCONNECTdials throughproxy.Handler.Dial; both refuse withoffline.ErrOfflinebefore any DNS lookup or connection while the switch is on. - Whatever is cached is served, however old. Every side's
ttl()is frozen, and apt'sCache-Control: max-age=0no longer forces a revalidation. Served as hits, not as stale-after-a-failed-revalidation, so no warning per request. - What the cache cannot answer fails fast.
offline.Middlewareturns the 502 of a failed upstream fetch into a 503 withRetry-Afterand "pkgproxy is offline".
Commits: the internal/offline package (+ httpheader constants), the wiring and the harness, the admin route. Design: new §21.
Test harness (internal/gateway/offline_test.go): every side (apt, snap, go, registry, lxd) behind the real gateway with its own fake upstream. It warms the caches online, advances a fake clock by a day, goes offline and asserts byte-identical answers and zero upstream requests. http.DefaultTransport is replaced by one that fails the test on any use, so a call site that bypasses the switch is caught. A control run shows the same requests do reach upstream a day later while online (so the test really tests the freezing), and another test pins the 503s for misses (including CONNECT). I checked that disabling the TTL freeze, or the transport refusal, makes them fail.
Known limit, by design for now: snap calls that are relayed uncached (POST /v2/snaps/refresh, find) answer 503, so snap install / refresh do not work offline even for a cached snap. That is the follow-up in #54 (closed).
pkgproxyctl offline [status|on|off|mode <mode>] is the command-line front end to the admin routes. Not done here: an offline indicator in the stats API / setup page.