fix(spur-net): cache OCI layers under the resolved image directory - #507
fix(spur-net): cache OCI layers under the resolved image directory#507maybeharshit wants to merge 2 commits into
Conversation
|
This PR is on top of PR #500 , wait for the merge of the previous PR then proceed with this one. |
|
Hi @maybeharshit , we've merged your previous PR #500 , please rebase this PR on top of latest upstream main branch. |
Keep rootless image imports cacheable by colocating layers with the resolved image directory while preserving explicit overrides and surfacing nonfatal cache failures. Co-authored-by: Cursor <cursoragent@cursor.com>
9f697f7 to
443e8d7
Compare
yansun1996
left a comment
There was a problem hiding this comment.
Thanks for this — deriving the layer cache from the resolved output_dir is the right fix for #345, and degrading gracefully instead of silently swallowing cache failures is a nice touch. A few suggestions before merge, mostly an edge case and test coverage — details inline.
| let result = pull_and_extract(&image_ref, &rootfs_dir).await; | ||
| let cache_dir = layer_cache_dir( | ||
| output_dir, | ||
| std::env::var_os("SPUR_IMAGE_CACHE").map(PathBuf::from), |
There was a problem hiding this comment.
var_os(...).map(PathBuf::from) treats SPUR_IMAGE_CACHE="" as an explicit empty override, so layer_cache_dir returns "", create_dir_all("") fails, and the cache is disabled — which reintroduces the "re-download every layer" symptom this PR fixes. The sibling env resolver in spur-cli/src/image.rs guards this (if !dir.is_empty()); would it be worth mirroring that here?
std::env::var_os("SPUR_IMAGE_CACHE")
.filter(|v| !v.is_empty())
.map(PathBuf::from),There was a problem hiding this comment.
Good catch — fixed in 1d48fa3. Rather than filtering at the call site, the guard now lives inside layer_cache_dir itself, so every caller (including the tests) goes through the same rule:
match override_dir.filter(|dir| !dir.is_empty()) {
Some(dir) => PathBuf::from(dir),
None => output_dir.join(".layers"),
}An empty SPUR_IMAGE_CACHE is now treated as unset and falls back to <output_dir>/.layers, matching the SPUR_IMAGE_DIR handling in spur-cli/src/image.rs.
| let cache_enabled = match std::fs::create_dir_all(cache_dir) { | ||
| Ok(()) => true, | ||
| Err(error) => { | ||
| if tracing::enabled!(Level::WARN) { |
There was a problem hiding this comment.
This tracing::enabled!(WARN) → warn! else eprintln! block is duplicated verbatim at the layer-write site below — worth pulling into a small shared helper.
One subtlety with the gate: tracing::enabled!(Level::WARN) is also false when a subscriber is installed but WARN is filtered out. In that case warn! is suppressed and the eprintln! branch never runs, so the warning is lost in exactly the situation the fallback was meant to cover. Always calling warn! (and letting the CLI install a subscriber) may be simpler and avoids that gap.
There was a problem hiding this comment.
Agreed on both points — the tracing::enabled! gate is gone entirely as of 1d48fa3.
Cache handling now sits behind a small LayerCache type that calls warn! unconditionally at both the create and write sites, so there is no duplicated block and no way for a subscriber with WARN filtered out to swallow the message without the eprintln! fallback firing.
To make sure those warnings actually reach users, spur-cli now installs a stderr subscriber in main, defaulting to warn and honouring RUST_LOG for more verbosity. Previously the CLI had no subscriber at all, which is what the eprintln! fallback was compensating for.
| } | ||
|
|
||
| #[test] | ||
| fn layer_cache_defaults_below_output_directory() { |
There was a problem hiding this comment.
These two tests exercise only the pure layer_cache_dir helper, so they'd still pass if the core of the fix (the env read in pull_image, the create_dir_all failure handling, and the Option gating on the layer read/write) were reverted. Could the actual degradation path get coverage? Extracting a small synchronous cache read/write seam would let you assert the disable-on-unwritable and round-trip behaviors with tempfile, no network needed. At minimum, an empty-SPUR_IMAGE_CACHE case here would lock in the guard suggested above.
There was a problem hiding this comment.
You were right that the original tests would have survived a revert of the actual fix. 1d48fa3 extracts the synchronous read/write seam you suggested — a LayerCache type with open/read_layer/write_layer — so the degradation paths are now covered directly with tempfile, no network needed:
layer_cache_round_trips_layers— write then read back through the cache.layer_cache_disabled_when_directory_cannot_be_created—openon an unwritable parent degrades to a disabled cache, and reads/writes become no-ops instead of errors.layer_cache_write_failure_leaves_pull_usable— a failing write does not propagate, so the pull continues.layer_cache_ignores_empty_environment_override— locks in the empty-value guard from the thread above.
All 40 spur-net tests pass, along with workspace clippy and the full test suite.
Treat an empty SPUR_IMAGE_CACHE as unset so it falls back to the image directory instead of disabling the cache, and move the cache behind a LayerCache type so create/read/write degradation is unit testable. Cache failures now emit a single unconditional warning; spur-cli installs a stderr tracing subscriber so those warnings reach users instead of being dropped, which also removes the duplicated eprintln fallback that was skipped whenever a subscriber filtered out WARN. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@yansun1996 thanks for the review — all three points are addressed in 1d48fa3, with details in the inline threads. Summary:
Validated with |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #507 +/- ##
==========================================
+ Coverage 74.23% 74.35% +0.12%
==========================================
Files 165 165
Lines 57595 59429 +1834
==========================================
+ Hits 42755 44186 +1431
- Misses 14840 15243 +403 🚀 New features to boost your workflow:
|
Motivation
Non-root image imports fall back from
/var/spool/spur/imagesto~/.spur/images, but the OCI layer cache remained hardcoded under the system directory. Cache writes consequently failed silently, causing every fresh import to download all layers again.Fixes #345.
Technical Details
<output_dir>/.layers.SPUR_IMAGE_CACHEas an explicit override.Test Plan
spur-netunit tests.Test Result
spur-nettests passed.~/.spur/images/.layers;SPUR_IMAGE_CACHEremained effective;Submission Checklist