Make snapshot rm clean up the remote by default
snapshot rm <id> now does the full cleanup: removes the local index entry, strips the snapshot's metadata from the destination store, and prunes any blobs that were only referenced by the just-removed manifest. The --remote flag is retired; --local-only opts out for the rare case where the user wants to forget a snapshot locally without touching the remote. If the destination store is unreachable, the local-DB removal still completes and a warning is emitted; the user can rerun 'vaultik prune' to retry the remote half later. RemoveAllSnapshots gets the same treatment: after deleting every snapshot's metadata (local + remote + orphan keys), an automatic blob prune sweep removes the now-unreferenced blob set.
This commit is contained in:
@@ -188,7 +188,11 @@ func addBlob(t *testing.T, store *testStorer, hash string) {
|
||||
// Unit Tests for RemoveSnapshot
|
||||
// ============================================================================
|
||||
|
||||
func TestRemoveSnapshot_LocalOnly(t *testing.T) {
|
||||
// TestRemoveSnapshot_LocalOnly_PreservesRemote confirms that
|
||||
// --local-only opts out of the remote-cleanup half: the snapshot is
|
||||
// removed from the local index, but the remote metadata and blobs are
|
||||
// untouched.
|
||||
func TestRemoveSnapshot_LocalOnly_PreservesRemote(t *testing.T) {
|
||||
log.Initialize(log.Config{})
|
||||
|
||||
store := newTestStorer()
|
||||
@@ -199,49 +203,61 @@ func TestRemoveSnapshot_LocalOnly(t *testing.T) {
|
||||
|
||||
tv := vaultik.NewForTesting(store)
|
||||
|
||||
opts := &vaultik.RemoveOptions{Force: true, LocalOnly: true}
|
||||
result, err := tv.RemoveSnapshot("snapshot-001", opts)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "snapshot-001", result.SnapshotID)
|
||||
assert.False(t, result.RemoteRemoved)
|
||||
assert.Equal(t, 0, result.BlobsDeleted)
|
||||
|
||||
assert.True(t, store.hasKey("blobs/aa/aa/"+blobA))
|
||||
assert.True(t, store.hasKey(remoteKeyPath("snapshot-001", "manifest.json.zst")))
|
||||
|
||||
assert.Contains(t, tv.Stdout.String(), "Removed snapshot 'snapshot-001' from local database")
|
||||
}
|
||||
|
||||
// TestRemoveSnapshot_DefaultFullCleanup is the canonical case: no
|
||||
// flags. The local-DB entry is removed, the snapshot's metadata is
|
||||
// removed from the destination store, and any blob that was unique to
|
||||
// this snapshot (i.e. not referenced by any remaining manifest) is
|
||||
// pruned from the destination store too.
|
||||
func TestRemoveSnapshot_DefaultFullCleanup(t *testing.T) {
|
||||
log.Initialize(log.Config{})
|
||||
|
||||
store := newTestStorer()
|
||||
|
||||
blobUnique := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
blobShared := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
|
||||
addManifest(t, store, "snapshot-001", []string{blobUnique, blobShared})
|
||||
addManifest(t, store, "snapshot-002", []string{blobShared})
|
||||
addBlob(t, store, blobUnique)
|
||||
addBlob(t, store, blobShared)
|
||||
|
||||
tv := vaultik.NewForTesting(store)
|
||||
|
||||
opts := &vaultik.RemoveOptions{Force: true}
|
||||
result, err := tv.RemoveSnapshot("snapshot-001", opts)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "snapshot-001", result.SnapshotID)
|
||||
assert.False(t, result.RemoteRemoved)
|
||||
|
||||
// Blobs should NOT be deleted (that's what prune is for)
|
||||
assert.True(t, store.hasKey("blobs/aa/aa/"+blobA))
|
||||
// Remote metadata should NOT be deleted (no --remote flag)
|
||||
assert.True(t, store.hasKey(remoteKeyPath("snapshot-001", "manifest.json.zst")))
|
||||
|
||||
// Verify output
|
||||
assert.Contains(t, tv.Stdout.String(), "Removed snapshot 'snapshot-001' from local database")
|
||||
}
|
||||
|
||||
func TestRemoveSnapshot_WithRemote(t *testing.T) {
|
||||
log.Initialize(log.Config{})
|
||||
|
||||
store := newTestStorer()
|
||||
|
||||
blobA := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
addManifest(t, store, "snapshot-001", []string{blobA})
|
||||
addBlob(t, store, blobA)
|
||||
|
||||
tv := vaultik.NewForTesting(store)
|
||||
|
||||
opts := &vaultik.RemoveOptions{Force: true, Remote: true}
|
||||
result, err := tv.RemoveSnapshot("snapshot-001", opts)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "snapshot-001", result.SnapshotID)
|
||||
assert.True(t, result.RemoteRemoved)
|
||||
assert.Equal(t, 1, result.BlobsDeleted, "exactly the unique blob should be deleted")
|
||||
|
||||
// Blobs should NOT be deleted
|
||||
assert.True(t, store.hasKey("blobs/aa/aa/"+blobA))
|
||||
// Remote metadata SHOULD be deleted
|
||||
// Snapshot-001's metadata gone.
|
||||
assert.False(t, store.hasKey(remoteKeyPath("snapshot-001", "manifest.json.zst")))
|
||||
// Snapshot-002 untouched.
|
||||
assert.True(t, store.hasKey(remoteKeyPath("snapshot-002", "manifest.json.zst")))
|
||||
// Unique blob deleted.
|
||||
assert.False(t, store.hasKey("blobs/aa/aa/"+blobUnique))
|
||||
// Shared blob preserved (still referenced by snapshot-002).
|
||||
assert.True(t, store.hasKey("blobs/bb/bb/"+blobShared))
|
||||
|
||||
// Verify output mentions prune
|
||||
assert.Contains(t, tv.Stdout.String(), "Removed snapshot 'snapshot-001' from local database")
|
||||
assert.Contains(t, tv.Stdout.String(), "Removed snapshot metadata from remote storage")
|
||||
assert.Contains(t, tv.Stdout.String(), "Run 'vaultik prune' to remove orphaned blobs")
|
||||
out := tv.Stdout.String()
|
||||
assert.Contains(t, out, "Removed snapshot 'snapshot-001' from local database")
|
||||
assert.Contains(t, out, "Removed snapshot metadata from remote storage")
|
||||
assert.Contains(t, out, "Removed 1 unreferenced blob")
|
||||
}
|
||||
|
||||
func TestRemoveSnapshot_DryRun(t *testing.T) {
|
||||
@@ -257,18 +273,16 @@ func TestRemoveSnapshot_DryRun(t *testing.T) {
|
||||
|
||||
tv := vaultik.NewForTesting(store)
|
||||
|
||||
opts := &vaultik.RemoveOptions{Force: true, DryRun: true, Remote: true}
|
||||
opts := &vaultik.RemoveOptions{Force: true, DryRun: true}
|
||||
result, err := tv.RemoveSnapshot("snapshot-001", opts)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.DryRun)
|
||||
|
||||
// Nothing should be deleted
|
||||
assert.Equal(t, initialCount, store.keyCount())
|
||||
assert.True(t, store.hasKey("blobs/aa/aa/"+blobA))
|
||||
assert.True(t, store.hasKey(remoteKeyPath("snapshot-001", "manifest.json.zst")))
|
||||
|
||||
// Verify dry run message
|
||||
assert.Contains(t, tv.Stdout.String(), "[Dry run - no changes made]")
|
||||
}
|
||||
|
||||
@@ -300,22 +314,22 @@ func TestRemoveAllSnapshots_WithForce(t *testing.T) {
|
||||
|
||||
tv := vaultik.NewForTesting(store)
|
||||
|
||||
opts := &vaultik.RemoveOptions{All: true, Force: true, Remote: true}
|
||||
opts := &vaultik.RemoveOptions{All: true, Force: true}
|
||||
result, err := tv.RemoveAllSnapshots(opts)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result.SnapshotsRemoved, 2)
|
||||
assert.True(t, result.RemoteRemoved)
|
||||
assert.Equal(t, 1, result.BlobsDeleted)
|
||||
|
||||
// Blobs should NOT be deleted
|
||||
assert.True(t, store.hasKey("blobs/aa/aa/"+blobA))
|
||||
// Remote metadata SHOULD be deleted
|
||||
assert.False(t, store.hasKey("blobs/aa/aa/"+blobA))
|
||||
assert.False(t, store.hasKey(remoteKeyPath("snapshot-001", "manifest.json.zst")))
|
||||
assert.False(t, store.hasKey(remoteKeyPath("snapshot-002", "manifest.json.zst")))
|
||||
|
||||
// Verify output
|
||||
assert.Contains(t, tv.Stdout.String(), "Removed 2 snapshot(s)")
|
||||
assert.Contains(t, tv.Stdout.String(), "Run 'vaultik prune' to remove orphaned blobs")
|
||||
out := tv.Stdout.String()
|
||||
assert.Contains(t, out, "Removed 2 snapshot(s)")
|
||||
assert.Contains(t, out, "Removed snapshot metadata from remote storage")
|
||||
assert.Contains(t, out, "Removed 1 unreferenced blob")
|
||||
}
|
||||
|
||||
func TestRemoveAllSnapshots_DryRun(t *testing.T) {
|
||||
@@ -329,20 +343,18 @@ func TestRemoveAllSnapshots_DryRun(t *testing.T) {
|
||||
|
||||
tv := vaultik.NewForTesting(store)
|
||||
|
||||
// --remote is required to enumerate orphan remote keys; without
|
||||
// it, RemoveAll only acts on local snapshots, and NewForTesting
|
||||
// has no local DB.
|
||||
opts := &vaultik.RemoveOptions{All: true, Force: true, DryRun: true, Remote: true}
|
||||
// Default (no LocalOnly) enumerates the orphan remote keys, which
|
||||
// matches what NewForTesting has — local DB is empty, so the two
|
||||
// addManifest calls land as orphan remote keys.
|
||||
opts := &vaultik.RemoveOptions{All: true, Force: true, DryRun: true}
|
||||
result, err := tv.RemoveAllSnapshots(opts)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.DryRun)
|
||||
assert.Len(t, result.SnapshotsRemoved, 2)
|
||||
|
||||
// Nothing should be deleted
|
||||
assert.Equal(t, initialCount, store.keyCount())
|
||||
|
||||
// Verify dry run message
|
||||
assert.Contains(t, tv.Stdout.String(), "[Dry run - no changes made]")
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user