From 0cf675ba202afb44dad88cbf0db26fb7e9288a38 Mon Sep 17 00:00:00 2001 From: Juan Antonio Osorio Date: Tue, 24 Mar 2026 13:32:46 +0200 Subject: [PATCH] Fix legacy ref files showing as orphans in cache list Legacy-format ref files (written by v0.0.24, digest-only) were not recognized by buildRefMap(), causing List() to report them as orphans and gc --dry-run to incorrectly claim they would be deleted (while actual GC via liveDigests() correctly preserved them). Two fixes: - LookupRef now upgrades legacy refs to extended format on cache hit - buildRefMap now marks legacy refs as referenced with a placeholder Co-Authored-By: Claude Opus 4.6 (1M context) --- image/cache.go | 13 ++++++++++--- image/cache_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/image/cache.go b/image/cache.go index 5377486..d872449 100644 --- a/image/cache.go +++ b/image/cache.go @@ -327,11 +327,14 @@ func (c *Cache) buildRefMap() map[string][]string { if digest == "" { continue } - // Skip empty image refs from legacy-format files. The entry still - // counts as referenced for GC (via liveDigests), but we don't add - // an empty string to the Refs slice. if imageRef != "" { refMap[digest] = append(refMap[digest], imageRef) + } else if _, exists := refMap[digest]; !exists { + // Legacy-format ref (digest only, no image name). Mark the + // digest as referenced so it is not reported as orphaned. + // The placeholder is replaced once the ref is upgraded to + // extended format (e.g. on next LookupRef hit). + refMap[digest] = []string{"(unknown image)"} } } return refMap @@ -424,6 +427,10 @@ func (c *Cache) LookupRef(imageRef string) *RootFS { return nil } + // Upgrade legacy-format ref files (digest only) to extended format + // (imageRef\tdigest) so that List/GC can recover the image name. + c.putRef(imageRef, digest) + rootfsPath, ok := c.Get(digest) if !ok { return nil diff --git a/image/cache_test.go b/image/cache_test.go index d7c2498..a29beff 100644 --- a/image/cache_test.go +++ b/image/cache_test.go @@ -353,10 +353,10 @@ func TestCache_List_LegacyRefFormat(t *testing.T) { require.NoError(t, err) require.Len(t, entries, 1) - // Legacy format: entry is referenced (not orphaned) but imageRef - // is not recoverable, so Refs is empty (no empty strings). + // Legacy format: entry is referenced (not orphaned) but the original + // image name is not recoverable, so a placeholder is used. assert.Equal(t, "sha256:legacy", entries[0].Digest) - assert.Empty(t, entries[0].Refs) + assert.Equal(t, []string{"(unknown image)"}, entries[0].Refs) } // --- GC tests --- @@ -584,6 +584,47 @@ func TestCache_GC_CorruptRefFiles(t *testing.T) { assert.Equal(t, 1, removed) } +func TestLookupRef_UpgradesLegacyRef(t *testing.T) { + t.Parallel() + + cacheDir := t.TempDir() + c := NewCache(cacheDir) + + digest := "sha256:legacyupgrade" + imageRef := "ghcr.io/org/image:latest" + + // Create a rootfs entry with an OCI config. + rootfsDir := filepath.Join(cacheDir, "sha256-legacyupgrade") + require.NoError(t, os.MkdirAll(rootfsDir, 0o700)) + require.NoError(t, os.WriteFile( + filepath.Join(rootfsDir, ".oci-config.json"), + []byte(`{"Entrypoint":["/bin/sh"]}`), 0o600, + )) + + // Write a legacy-format ref file (digest only, no imageRef). + refsDir := filepath.Join(cacheDir, "refs") + require.NoError(t, os.MkdirAll(refsDir, 0o700)) + p := c.refPath(imageRef) + require.NoError(t, os.WriteFile(p, []byte(digest+"\n"), 0o600)) + + // LookupRef should succeed (legacy format is readable). + result := c.LookupRef(imageRef) + require.NotNil(t, result) + assert.Equal(t, rootfsDir, result.Path) + + // After LookupRef, the ref file should be upgraded to extended format. + data, err := os.ReadFile(p) + require.NoError(t, err) + assert.Equal(t, imageRef+"\t"+digest+"\n", string(data), + "LookupRef should upgrade legacy ref to extended format") + + // List should now show the real image name instead of (unknown image). + entries, err := c.List() + require.NoError(t, err) + require.Len(t, entries, 1) + assert.Equal(t, []string{imageRef}, entries[0].Refs) +} + func TestGetRef_BackwardCompatible(t *testing.T) { t.Parallel()