From 325ec50cc0ea27110a3e77b718340443b493594b Mon Sep 17 00:00:00 2001 From: CMGS Date: Tue, 29 Sep 2026 03:30:19 +0800 Subject: [PATCH] fix: reclaim clone-source copies no pod on the node clones from The reclaimer treated every unused vk- snapshot as a VM's hibernate copy and verified it against :hibernate in the registry. A clone-source copy of a non-default tag or a digest carries a colon in its local name, so that lookup built an invalid reference, the error was not errStaleLocalSnapshot, and the copy was kept forever with no log line. #118 made every digest clone source land on this path; the kit's N28 run left two 1 GiB digest copies on the node. VM names never contain a colon, so a colon in the name marks a pulled clone-source copy: re-pullable, owned by no VM, and safe to drop once no pod on the node clones from it. Verify errors that are not staleness now log at Warn instead of vanishing. --- provider/cocoon/gc.go | 11 ++++++++++- provider/cocoon/gc_test.go | 11 +++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/provider/cocoon/gc.go b/provider/cocoon/gc.go index 45bceb8..986f95d 100644 --- a/provider/cocoon/gc.go +++ b/provider/cocoon/gc.go @@ -40,9 +40,18 @@ func (p *Provider) reclaimLocalSnapshots(ctx context.Context) { p.removeSnapshotDetached(ctx, s.Name) continue } - if _, _, verifyErr := p.verifyLocalSnapshot(ctx, vmName, &s); errors.Is(verifyErr, errStaleLocalSnapshot) { + if strings.Contains(vmName, ":") { + logger.Infof(ctx, "reclaim local snapshot %s: no pod on this node clones from it", s.Name) + p.removeSnapshotDetached(ctx, s.Name) + continue + } + _, _, verifyErr := p.verifyLocalSnapshot(ctx, vmName, &s) + switch { + case errors.Is(verifyErr, errStaleLocalSnapshot): logger.Infof(ctx, "reclaim local snapshot %s: %v", s.Name, verifyErr) p.removeLocalSnapshots(ctx, vmName) + case verifyErr != nil: + logger.Warnf(ctx, "keep local snapshot %s: verify against the registry: %v", s.Name, verifyErr) } } } diff --git a/provider/cocoon/gc_test.go b/provider/cocoon/gc_test.go index 47c5d90..a873250 100644 --- a/provider/cocoon/gc_test.go +++ b/provider/cocoon/gc_test.go @@ -3,10 +3,12 @@ package cocoon import ( "errors" "slices" + "strings" "testing" "github.com/cocoonstack/cocoon-common/meta" "github.com/cocoonstack/cocoon-common/oci" + "github.com/cocoonstack/cocoon-common/ociutil" "github.com/cocoonstack/vk-cocoon/vm" ) @@ -15,6 +17,11 @@ func TestTheReclaimerDropsOnlyWhatNoPodOnTheNodeCanWakeFrom(t *testing.T) { importName := vmName + meta.HibernateImportSuffix self := meta.VMSpec{VMName: vmName} cloner := meta.VMSpec{VMName: "vk-ns-clone-0-9a1f3c", Image: vmName} + taggedCopy := vmName + ":v2" + tagCloner := meta.VMSpec{VMName: "vk-ns-clone-0-9a1f3c", Image: taggedCopy} + digestRef := vmName + "@sha256:" + strings.Repeat("ab", 32) + digestCopy := localSnapshotName(ociutil.ParseRef(digestRef)) + digestCloner := meta.VMSpec{VMName: "vk-ns-clone-0-9a1f3c", Image: digestRef} tests := []struct { name string local string @@ -32,6 +39,10 @@ func TestTheReclaimerDropsOnlyWhatNoPodOnTheNodeCanWakeFrom(t *testing.T) { {"an import no pod on this node wakes", importName, meta.VMSpec{}, newWakeVerifyRegistry(t, "SNAP-1"), []string{importName}}, {"an import a pod on this node wakes", importName, self, newWakeVerifyRegistry(t, "SNAP-1"), nil}, {"no registry configured", vmName, meta.VMSpec{}, nil, nil}, + {"a tagged clone-source copy no pod clones from", taggedCopy, meta.VMSpec{}, registryDown(t), []string{taggedCopy}}, + {"a tagged clone-source copy a pod clones from", taggedCopy, tagCloner, registryDown(t), nil}, + {"a digest clone-source copy no pod clones from", digestCopy, meta.VMSpec{}, registryDown(t), []string{digestCopy}}, + {"a digest clone-source copy a pod clones from", digestCopy, digestCloner, registryDown(t), nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {