check / check (push) Waiting to run
remote nuke deleted only the snapshot metadata and blobs the snapshot and blob listings recognise, so a snapshot's database whose manifest was never uploaded, and any name under blobs/ that is not a blob hash, survived while nuke reported the store empty. After removing every snapshot, nuke now deletes every object listed under metadata/ and blobs/, whatever its name, in place of the blob prune step. A delete that fails is logged and the others still run. A listing afterwards decides the result: nuke reports the store empty only when it finds nothing, and otherwise names each object left and fails. Judgement call: nuke no longer prints the blob prune summary. Model: opus-5-5
177 lines
5.6 KiB
Go
177 lines
5.6 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"context"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/snapshot"
|
|
"sneak.berlin/go/vaultik/internal/storage/faultstore"
|
|
)
|
|
|
|
// TestNukeRemoteLeavesNoFiles checks that remote nuke leaves no file
|
|
// under a file:// destination, including the `.partial` files uploads
|
|
// killed part-way leave next to a snapshot's metadata and next to where a
|
|
// blob would have been, a snapshot's database whose manifest was never
|
|
// uploaded, and a file under blobs/ whose name is not a blob hash.
|
|
func TestNukeRemoteLeavesNoFiles(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
storeDir := filepath.Join(t.TempDir(), "store")
|
|
v, repos, out := backUpToFileDestination(ctx, t, storeDir)
|
|
|
|
snapshots, err := repos.Snapshots.ListRecent(ctx, listRecentTestLimit)
|
|
require.NoError(t, err)
|
|
require.Len(t, snapshots, 1)
|
|
|
|
snapshotKey := snapshot.RemoteSnapshotKey(snapshots[0].ID.String())
|
|
hash := testBlobHashA
|
|
leftovers := []string{
|
|
filepath.Join(storeDir, "metadata", snapshotKey,
|
|
"db.zst.age-123456.partial"),
|
|
filepath.Join(storeDir, "blobs", hash[:2], hash[2:4],
|
|
hash+"-123456.partial"),
|
|
filepath.Join(storeDir, "metadata",
|
|
snapshot.RemoteSnapshotKey("no-manifest"), "db.zst.age"),
|
|
filepath.Join(storeDir, "blobs", "0b", "01", ".DS_Store"),
|
|
}
|
|
|
|
for _, leftover := range leftovers {
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(leftover), 0o750))
|
|
require.NoError(t, os.WriteFile(leftover, []byte("half an upload"), 0o600))
|
|
}
|
|
|
|
require.NoError(t, v.NukeRemote(true))
|
|
|
|
var files []string
|
|
|
|
err = filepath.WalkDir(storeDir,
|
|
func(path string, entry fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !entry.IsDir() {
|
|
files = append(files, path)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, files)
|
|
|
|
// Nuke deletes the blobs itself, so advice to run prune is wrong.
|
|
assert.Contains(t, out.String(), "Backup destination store is now empty.")
|
|
assert.NotContains(t, out.String(), "vaultik prune")
|
|
}
|
|
|
|
// TestNukeRemoteFailsWhenADeleteFails checks that remote nuke returns an
|
|
// error when a snapshot's metadata or a blob cannot be deleted, does not
|
|
// report the store empty, and advises running nuke again: prune deletes
|
|
// no metadata.
|
|
func TestNukeRemoteFailsWhenADeleteFails(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
for _, refusedPrefix := range []string{"metadata/", "blobs/"} {
|
|
t.Run(refusedPrefix, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
storeDir := filepath.Join(t.TempDir(), "store")
|
|
v, _, out := backUpToFileDestination(ctx, t, storeDir)
|
|
|
|
store := faultstore.New(v.Storage)
|
|
store.OnDelete = func(key string) bool {
|
|
return strings.HasPrefix(key, refusedPrefix)
|
|
}
|
|
v.Storage = store
|
|
|
|
require.Error(t, v.NukeRemote(true))
|
|
|
|
assert.NotContains(t, out.String(), "now empty")
|
|
assert.Contains(t, out.String(), "vaultik remote nuke --force")
|
|
assert.NotContains(t, out.String(), "vaultik prune")
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestNukeRemoteNamesWhatIsLeft checks that remote nuke, when an object
|
|
// it does not recognise cannot be deleted, does not report the store
|
|
// empty but names that object and fails.
|
|
func TestNukeRemoteNamesWhatIsLeft(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
storeDir := filepath.Join(t.TempDir(), "store")
|
|
v, _, out := backUpToFileDestination(ctx, t, storeDir)
|
|
|
|
const leftKey = "blobs/0b/01/.DS_Store"
|
|
|
|
leftPath := filepath.Join(storeDir, filepath.FromSlash(leftKey))
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(leftPath), 0o750))
|
|
require.NoError(t, os.WriteFile(leftPath, []byte("finder"), 0o600))
|
|
|
|
store := faultstore.New(v.Storage)
|
|
store.OnDelete = func(key string) bool { return key == leftKey }
|
|
v.Storage = store
|
|
|
|
require.Error(t, v.NukeRemote(true))
|
|
|
|
assert.Contains(t, out.String(), leftKey)
|
|
assert.NotContains(t, out.String(), "now empty")
|
|
assert.Contains(t, out.String(), "vaultik remote nuke --force")
|
|
}
|
|
|
|
// TestNukeRemoteRefusesAnotherDestinationWithoutRetryAdvice checks that
|
|
// remote nuke, when the local index belongs to a different destination
|
|
// than the configured one, fails without advising to run nuke again:
|
|
// that run would fail the same way.
|
|
func TestNukeRemoteRefusesAnotherDestinationWithoutRetryAdvice(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
storeDir := filepath.Join(t.TempDir(), "store")
|
|
v, _, out := backUpToFileDestination(ctx, t, storeDir)
|
|
|
|
v.Config.StorageURL = "file://" + filepath.Join(t.TempDir(), "elsewhere")
|
|
|
|
err := v.NukeRemote(true)
|
|
|
|
require.ErrorContains(t, err, "bound to a different backup destination")
|
|
assert.NotContains(t, out.String(), "vaultik remote nuke --force")
|
|
}
|
|
|
|
// TestNukeRemoteFailsWhenALocalSnapshotCannotBeRemoved checks that a
|
|
// snapshot remote nuke cannot remove from the local index fails the
|
|
// run: nuke then leaves that snapshot's metadata on the destination
|
|
// store.
|
|
func TestNukeRemoteFailsWhenALocalSnapshotCannotBeRemoved(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
storeDir := filepath.Join(t.TempDir(), "store")
|
|
v, _, out := backUpToFileDestination(ctx, t, storeDir)
|
|
|
|
_, err := v.DB.Conn().ExecContext(ctx, `
|
|
CREATE TRIGGER refuse_snapshot_delete BEFORE DELETE ON snapshots
|
|
BEGIN SELECT RAISE(ABORT, 'simulated index error'); END`)
|
|
require.NoError(t, err)
|
|
|
|
require.Error(t, v.NukeRemote(true))
|
|
|
|
assert.NotContains(t, out.String(), "now empty")
|
|
assert.Contains(t, out.String(), "vaultik remote nuke --force")
|
|
}
|