Bind the local index to its backup destination

The local index tracks which chunks and blobs already exist at the
backup destination. Nothing was recording *which* destination, so
changing storage_url and running a backup left the scanner treating
every already-seen chunk as still-present at the new (empty) location.
Uploads were skipped silently and the resulting snapshots pointed at
blobs that don't exist at the new destination.

Fix: record storage_url in a new local_meta key-value table on first
mutating command, and refuse to proceed when the configured URL later
differs from the stored one. The error explains the two recovery
paths (revert the config, or run 'vaultik database purge' to discard
the index and rebuild from a fresh full backup).

Wired into snapshot create / prune / snapshot remove / snapshot purge
/ snapshot cleanup. Read-only inspection commands (snapshot list,
remote info, store info) are exempt.
This commit is contained in:
2026-07-02 16:35:41 +02:00
parent fda6d7a7eb
commit d330f9f031
8 changed files with 304 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
package vaultik_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/vaultik/internal/config"
"sneak.berlin/go/vaultik/internal/database"
"sneak.berlin/go/vaultik/internal/log"
"sneak.berlin/go/vaultik/internal/vaultik"
)
// buildBindTestVaultik returns a minimal Vaultik wired with a real
// in-memory DB and a config carrying the given StorageURL. Enough to
// exercise EnsureStorageBinding without spinning up storage or fx.
func buildBindTestVaultik(t *testing.T, storageURL string) (*vaultik.TestVaultik, *database.Repositories) {
t.Helper()
db, err := database.NewTestDB()
require.NoError(t, err)
t.Cleanup(func() { _ = db.Close() })
repos := database.NewRepositories(db)
tv := vaultik.NewForTesting(nil)
tv.Repositories = repos
tv.DB = db
tv.Config = &config.Config{StorageURL: storageURL}
tv.SetContext(context.Background())
return tv, repos
}
func TestEnsureStorageBinding_FreshDBRecordsURL(t *testing.T) {
log.Initialize(log.Config{})
tv, repos := buildBindTestVaultik(t, "file:///mnt/backups/new")
require.NoError(t, tv.EnsureStorageBinding())
got, err := repos.LocalMeta.Get(context.Background(), database.LocalMetaKeyStorageURL)
require.NoError(t, err)
assert.Equal(t, "file:///mnt/backups/new", got, "first call must record the configured URL")
}
func TestEnsureStorageBinding_MatchingURLPasses(t *testing.T) {
log.Initialize(log.Config{})
tv, repos := buildBindTestVaultik(t, "s3://bucket/prefix")
require.NoError(t, repos.LocalMeta.Set(context.Background(),
database.LocalMetaKeyStorageURL, "s3://bucket/prefix"))
require.NoError(t, tv.EnsureStorageBinding())
}
func TestEnsureStorageBinding_MismatchRefuses(t *testing.T) {
log.Initialize(log.Config{})
tv, repos := buildBindTestVaultik(t, "file:///mnt/backups/new")
require.NoError(t, repos.LocalMeta.Set(context.Background(),
database.LocalMetaKeyStorageURL, "file:///mnt/backups/old"))
err := tv.EnsureStorageBinding()
require.Error(t, err)
assert.Contains(t, err.Error(), "file:///mnt/backups/old")
assert.Contains(t, err.Error(), "file:///mnt/backups/new")
assert.Contains(t, err.Error(), "vaultik database purge")
}