Add storage.cache_artifacts to serve artifacts without storing them - #387
Conversation
With storage.passthrough enabled the proxy streams every artifact from upstream to the client and never writes it to storage or the cache database. Metadata filtering, cooldown and the denylist work as before, so the proxy can sit behind another cache (e.g. an Artifactory remote) purely as a policy layer without holding a second copy of every package. Artifacts whose digest is known up front (OCI, Swift, Helm) are verified while streaming. Their responses are sent chunked and the connection is aborted on a mismatch, so a client never receives a tampered artifact as a complete response. Passthrough is rejected together with scanning, direct_serve and mirror_api, and the mirror command refuses to run with it, since all of them depend on stored artifacts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
andrew
left a comment
There was a problem hiding this comment.
Two issues need fixing before merging:
-
serveArtifactonly aborts onErrArtifactDigestMismatch, so other upstream read errors produce successful truncated responses. I reproduced this through the running proxy with an OCI blob whose upstream declaredContent-Length: 100but sent five bytes, and an npm tarball whose upstream closed a chunked response without the final chunk. Normal mode returned 502 for both. Passthrough returned 200 withContent-Length: 5, bodyshort, and no client read error. The OCI response also retained the expected digest header without completing verification. Abort on any copy error and add HTTP tests asserting that clients see an incomplete response for both cases. -
Swift archive HEAD requests call
getCachedArtifactWithUpstreamHashdirectly, bypassing the passthrough guard inGetCachedArtifact. I cached a Swift archive in normal mode, restarted with passthrough enabled, and made the upstream archive return 404 while keeping its release metadata available. HEAD still returned the cached 200 and length without requesting the upstream archive; GET returned 404. Apply the passthrough guard to this cache path too and add a regression test covering an existing cache entry when passthrough is enabled.
| # filtering, cooldown and the denylist still apply. Useful when another | ||
| # cache sits in front of the proxy. Incompatible with direct_serve, | ||
| # scanning and mirror_api. | ||
| passthrough: false |
There was a problem hiding this comment.
Small naming nit: passthrough is a bit ambiguous for this project. Since the whole point of the proxy is filtering, "passthrough" can easily be read as "pass everything through untouched", i.e. no cooldown and no denylist, which is the opposite of what this mode does.
passthrough is also a bit misleading in the storage section. Storage and the database stay in use: metadata is still cached there, and so are the publish times cooldown needs. Only artifacts are no longer stored.
What about naming it after what actually changes, e.g. storage.cache_artifacts (default true, PROXY_STORAGE_CACHE_ARTIFACTS)? An enum would also work, if that fits the config style better: storage.artifacts: cache | passthrough. Not a blocker, happy to go with whatever you prefer.
- serveArtifact aborts the response on any body read error, and when fewer bytes than the declared size were written, not only on a digest mismatch. A failed or short upstream body in passthrough mode no longer reaches the client as a complete 200. - Streamed upstream read failures are logged and counted as stream_failed upstream errors before the response is aborted. - checkCache reports a miss when artifact caching is off, so every cache read path, including Swift archive HEAD requests, ignores entries stored before the mode was enabled. Denylisted versions are still rejected. - Rename storage.passthrough to storage.cache_artifacts (default true, PROXY_STORAGE_CACHE_ARTIFACTS), which names what actually changes. Tests cover an OCI blob shorter than its Content-Length and an npm tarball whose chunked response ends without the final chunk, both served through the HTTP handlers, plus a Swift HEAD request against a cached archive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Match the storage.cache_artifacts option: the field says what changes (artifacts are streamed instead of stored) and its zero value keeps the usual caching behaviour for every Proxy built without the server config, such as the mirror command and tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the thorough review — both issues are fixed.
@Markeli good call on the name — renamed to I also merged current |
|
Thanks! |
Closes #385
Summary
Adds storage.cache_artifacts: false (PROXY_STORAGE_CACHE_ARTIFACTS). When enabled, artifact downloads are streamed from upstream to the client and never
written to storage or the cache database. Metadata filtering, cooldown and the denylist work as before.
Use case: running the proxy behind another cache (e.g. a remote repository in an artifact manager) purely for cooldown, without keeping a
second copy of every package.
Details
GetOrFetchArtifactand the URL-based fetch path skip the cache lookup, request coalescing andstoreArtifact. They stream the upstreambody directly.
GetCachedArtifactreturns nothing.chunked. On a digest mismatch the read fails and the handler aborts the connection, so the client never gets a tampered artifact as a complete
response.
scanning.enabled,storage.direct_serveormirror_api, since all three need storedartifacts. The
mirrorcommand refuses to run with it.Tests
ServeArtifactaborts with noContent-Length.go test ./...,go vet ./...andgolangci-lint run ./...pass.