check / check (push) Waiting to run
prune, and the prune step of snapshot create --prune, counted a blob it could not delete and still exited 0. It now prints its summary or --json document and then returns an error, which replaces the "Failed to delete N blob(s)" line. remote nuke only warned when a snapshot's metadata could not be deleted, advised vaultik prune, which never removes metadata, and then reported the store empty. It now stops with an error, does not report the store empty, and says to run vaultik remote nuke --force again. It checks that the local index belongs to the configured destination before deleting anything, and that error carries no such advice. A successful nuke no longer advises prune. Model: opus-5-5
145 lines
4.4 KiB
Go
145 lines
4.4 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.
|
|
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"),
|
|
}
|
|
|
|
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")
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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")
|
|
}
|