check / check (push) Waiting to run
A snapshot ID is hostname_name_timestamp, and purge took the name to be everything between the first and the last underscore. With a hostname such as my_host the name home came out as host_home, so `snapshot purge --keep-latest --snapshot home` found nothing to delete and `snapshot create --prune` purged nothing without a message. The name is now read by removing the hostname stored with the snapshot, in the short form the ID uses, so both may contain underscores. This was chosen over rejecting underscores in `hostname` when the config loads, which would also stop restores on such a host. The purge consistency test stored a hostname that did not match its snapshot IDs; it now matches, as it always does in production. Model: opus-5-5
324 lines
9.0 KiB
Go
324 lines
9.0 KiB
Go
package vaultik_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/snapshot"
|
|
"sneak.berlin/go/vaultik/internal/types"
|
|
"sneak.berlin/go/vaultik/internal/vaultik"
|
|
)
|
|
|
|
// Snapshot IDs reused across the purge tests, and the hostname they were
|
|
// taken on.
|
|
const (
|
|
snapHostname = "testhost"
|
|
snapSystemT0 = "testhost_system_2026-01-01T00:00:00Z"
|
|
snapHomeT0 = "testhost_home_2026-01-01T00:00:00Z"
|
|
snapHomeT1 = "testhost_home_2026-01-01T01:00:00Z"
|
|
snapHomeT3 = "testhost_home_2026-01-01T03:00:00Z"
|
|
)
|
|
|
|
// setupPurgeTest creates a Vaultik instance with an in-memory database and mock
|
|
// storage pre-populated with the given snapshot IDs, all taken on hostname.
|
|
// Each snapshot is marked as completed. Remote metadata stubs are created so
|
|
// syncWithRemote keeps them.
|
|
func setupPurgeTest(
|
|
t *testing.T, hostname string, snapshotIDs []string,
|
|
) *vaultik.Vaultik {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
db, err := database.New(ctx, ":memory:")
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
|
|
repos := database.NewRepositories(db)
|
|
mockStorage := NewMockStorer()
|
|
|
|
// Insert each snapshot into the DB and create remote metadata stubs.
|
|
// Use timestamps parsed from snapshot IDs for realistic ordering.
|
|
for _, id := range snapshotIDs {
|
|
// Parse timestamp from the snapshot ID
|
|
parts := strings.Split(id, "_")
|
|
timestampStr := parts[len(parts)-1]
|
|
startedAt, err := time.Parse(time.RFC3339, timestampStr)
|
|
require.NoError(t, err, "parsing timestamp from snapshot ID %q", id)
|
|
|
|
completedAt := startedAt.Add(5 * time.Minute)
|
|
snap := &database.Snapshot{
|
|
ID: types.SnapshotID(id),
|
|
Hostname: types.Hostname(hostname),
|
|
VaultikVersion: testLabel,
|
|
StartedAt: startedAt,
|
|
CompletedAt: &completedAt,
|
|
}
|
|
err = repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error {
|
|
return repos.Snapshots.Create(ctx, tx, snap)
|
|
})
|
|
require.NoError(t, err, "creating snapshot %s", id)
|
|
|
|
// Create the remote metadata stub under the production layout so
|
|
// syncWithRemote keeps the local row. Production stores metadata
|
|
// under the hashed remote key, not the human snapshot ID.
|
|
metadataKey := "metadata/" + snapshot.RemoteSnapshotKey(id) +
|
|
"/manifest.json.zst"
|
|
err = mockStorage.Put(ctx, metadataKey, strings.NewReader("stub"))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
stdout := &bytes.Buffer{}
|
|
stderr := &bytes.Buffer{}
|
|
stdin := &bytes.Buffer{}
|
|
|
|
v := &vaultik.Vaultik{
|
|
Storage: mockStorage,
|
|
Repositories: repos,
|
|
DB: db,
|
|
Stdout: stdout,
|
|
Stderr: stderr,
|
|
Stdin: stdin,
|
|
}
|
|
v.SetContext(ctx)
|
|
|
|
return v
|
|
}
|
|
|
|
// listRemainingSnapshots returns IDs of all completed snapshots in the database.
|
|
func listRemainingSnapshots(t *testing.T, v *vaultik.Vaultik) []string {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
dbSnaps, err := v.Repositories.Snapshots.ListRecent(ctx, 10000)
|
|
require.NoError(t, err)
|
|
|
|
var ids []string
|
|
|
|
for _, s := range dbSnaps {
|
|
if s.CompletedAt != nil {
|
|
ids = append(ids, s.ID.String())
|
|
}
|
|
}
|
|
|
|
return ids
|
|
}
|
|
|
|
func TestPurgeKeepLatest_PerName(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
// Create snapshots for two different names: "home" and "system".
|
|
// With per-name --keep-latest, the latest of each should be kept.
|
|
snapshotIDs := []string{
|
|
snapSystemT0,
|
|
snapHomeT1,
|
|
"testhost_system_2026-01-01T02:00:00Z",
|
|
snapHomeT3,
|
|
"testhost_system_2026-01-01T04:00:00Z",
|
|
}
|
|
|
|
v := setupPurgeTest(t, snapHostname, snapshotIDs)
|
|
|
|
err := v.PurgeSnapshotsWithOptions(&vaultik.SnapshotPurgeOptions{
|
|
KeepLatest: true,
|
|
Force: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
remaining := listRemainingSnapshots(t, v)
|
|
|
|
// Should keep the latest of each name
|
|
assert.Len(t, remaining, 2, "should keep exactly 2 snapshots (one per name)")
|
|
assert.Contains(t, remaining, "testhost_system_2026-01-01T04:00:00Z",
|
|
"should keep latest system")
|
|
assert.Contains(t, remaining, snapHomeT3, "should keep latest home")
|
|
}
|
|
|
|
func TestPurgeKeepLatest_SingleName(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
// All snapshots have the same name — keep-latest should keep exactly one.
|
|
snapshotIDs := []string{
|
|
snapHomeT0,
|
|
snapHomeT1,
|
|
"testhost_home_2026-01-01T02:00:00Z",
|
|
}
|
|
|
|
v := setupPurgeTest(t, snapHostname, snapshotIDs)
|
|
|
|
err := v.PurgeSnapshotsWithOptions(&vaultik.SnapshotPurgeOptions{
|
|
KeepLatest: true,
|
|
Force: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
remaining := listRemainingSnapshots(t, v)
|
|
assert.Len(t, remaining, 1)
|
|
assert.Contains(t, remaining, "testhost_home_2026-01-01T02:00:00Z",
|
|
"should keep the newest")
|
|
}
|
|
|
|
func TestPurgeKeepLatest_WithNameFilter(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
// Use --name to filter purge to only "home" snapshots.
|
|
// "system" snapshots should be untouched.
|
|
snapshotIDs := []string{
|
|
snapSystemT0,
|
|
snapHomeT1,
|
|
"testhost_system_2026-01-01T02:00:00Z",
|
|
snapHomeT3,
|
|
"testhost_home_2026-01-01T04:00:00Z",
|
|
}
|
|
|
|
v := setupPurgeTest(t, snapHostname, snapshotIDs)
|
|
|
|
err := v.PurgeSnapshotsWithOptions(&vaultik.SnapshotPurgeOptions{
|
|
KeepLatest: true,
|
|
Force: true,
|
|
Names: []string{"home"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
remaining := listRemainingSnapshots(t, v)
|
|
|
|
// 2 system snapshots untouched + 1 latest home = 3
|
|
assert.Len(t, remaining, 3)
|
|
assert.Contains(t, remaining, snapSystemT0)
|
|
assert.Contains(t, remaining, "testhost_system_2026-01-01T02:00:00Z")
|
|
assert.Contains(t, remaining, "testhost_home_2026-01-01T04:00:00Z")
|
|
}
|
|
|
|
func TestPurgeKeepLatest_NoSnapshots(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
v := setupPurgeTest(t, snapHostname, nil)
|
|
|
|
err := v.PurgeSnapshotsWithOptions(&vaultik.SnapshotPurgeOptions{
|
|
KeepLatest: true,
|
|
Force: true,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPurgeKeepLatest_NameFilterNoMatch(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
snapshotIDs := []string{
|
|
snapSystemT0,
|
|
"testhost_system_2026-01-01T01:00:00Z",
|
|
}
|
|
|
|
v := setupPurgeTest(t, snapHostname, snapshotIDs)
|
|
|
|
err := v.PurgeSnapshotsWithOptions(&vaultik.SnapshotPurgeOptions{
|
|
KeepLatest: true,
|
|
Force: true,
|
|
Names: []string{"nonexistent"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// All snapshots should remain — the name filter matched nothing
|
|
remaining := listRemainingSnapshots(t, v)
|
|
assert.Len(t, remaining, 2)
|
|
}
|
|
|
|
func TestPurgeOlderThan_WithNameFilter(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
// Snapshots with different names and timestamps.
|
|
// --older-than should apply only to the named subset when --name is used.
|
|
snapshotIDs := []string{
|
|
"testhost_system_2020-01-01T00:00:00Z",
|
|
"testhost_home_2020-01-01T00:00:00Z",
|
|
snapSystemT0,
|
|
snapHomeT0,
|
|
}
|
|
|
|
v := setupPurgeTest(t, snapHostname, snapshotIDs)
|
|
|
|
// Purge only "home" snapshots older than 365 days
|
|
err := v.PurgeSnapshotsWithOptions(&vaultik.SnapshotPurgeOptions{
|
|
OlderThan: "365d",
|
|
Force: true,
|
|
Names: []string{"home"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
remaining := listRemainingSnapshots(t, v)
|
|
|
|
// Old system stays (not filtered by name), old home deleted, recent ones stay
|
|
assert.Len(t, remaining, 3)
|
|
assert.Contains(t, remaining, "testhost_system_2020-01-01T00:00:00Z")
|
|
assert.Contains(t, remaining, snapSystemT0)
|
|
assert.Contains(t, remaining, snapHomeT0)
|
|
}
|
|
|
|
func TestPurgeKeepLatest_ThreeNames(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
// Three different snapshot names with multiple snapshots each.
|
|
snapshotIDs := []string{
|
|
snapHomeT0,
|
|
"testhost_system_2026-01-01T01:00:00Z",
|
|
"testhost_media_2026-01-01T02:00:00Z",
|
|
"testhost_home_2026-01-01T03:00:00Z",
|
|
"testhost_system_2026-01-01T04:00:00Z",
|
|
"testhost_media_2026-01-01T05:00:00Z",
|
|
"testhost_home_2026-01-01T06:00:00Z",
|
|
}
|
|
|
|
v := setupPurgeTest(t, snapHostname, snapshotIDs)
|
|
|
|
err := v.PurgeSnapshotsWithOptions(&vaultik.SnapshotPurgeOptions{
|
|
KeepLatest: true,
|
|
Force: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
remaining := listRemainingSnapshots(t, v)
|
|
assert.Len(t, remaining, 3, "should keep one per name")
|
|
assert.Contains(t, remaining, "testhost_home_2026-01-01T06:00:00Z")
|
|
assert.Contains(t, remaining, "testhost_system_2026-01-01T04:00:00Z")
|
|
assert.Contains(t, remaining, "testhost_media_2026-01-01T05:00:00Z")
|
|
}
|
|
|
|
// A hostname may contain underscores, so the snapshot name cannot be found
|
|
// by splitting the ID at them. A purge by name must still select "docs".
|
|
func TestPurgeKeepLatest_HostnameWithUnderscore(t *testing.T) {
|
|
log.Initialize(log.Config{})
|
|
t.Parallel()
|
|
|
|
const (
|
|
system = "my_host_system_2026-01-01T00:00:00Z"
|
|
docsT1 = "my_host_docs_2026-01-01T01:00:00Z"
|
|
docsT2 = "my_host_docs_2026-01-01T02:00:00Z"
|
|
)
|
|
|
|
v := setupPurgeTest(t, "my_host", []string{system, docsT1, docsT2})
|
|
|
|
err := v.PurgeSnapshotsWithOptions(&vaultik.SnapshotPurgeOptions{
|
|
KeepLatest: true,
|
|
Force: true,
|
|
Names: []string{"docs"},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assert.ElementsMatch(t, []string{system, docsT2},
|
|
listRemainingSnapshots(t, v))
|
|
}
|