check / check (push) Waiting to run
A source host whose credentials allow only PUT, which the documents say is enough to back up, failed every backup after its first: the marker check looks the marker up with an S3 HEAD, which those credentials are refused, and any error other than not found was fatal. Such an error now means the check could not be made: it is logged at info level, so --cron stays silent, and the command goes on. Not found still stops it. The documents now say so, and that such a host does not notice an emptied destination, so vaultik database delete must be run on it after the destination is emptied. The fault-injecting test store gains a Stat hook for the test. Model: opus-5-5
76 lines
2.7 KiB
Go
76 lines
2.7 KiB
Go
package cli //nolint:testpackage // shares runEntry and the hermetic config helpers
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"sneak.berlin/go/vaultik/internal/database"
|
|
"sneak.berlin/go/vaultik/internal/snapshot"
|
|
)
|
|
|
|
// Words and flags of `snapshot purge` used to build argument vectors
|
|
// below.
|
|
const (
|
|
cmdPurge = "purge"
|
|
flagOlderThan = "--older-than"
|
|
flagForce = "--force"
|
|
)
|
|
|
|
// TestEntrySnapshotPurgeKeepsMetadataWhenLocalIndexDeleteFails makes
|
|
// the local index refuse to delete an old snapshot and checks that
|
|
// `snapshot purge` exits non-zero and leaves that snapshot's metadata on
|
|
// the destination store. It used to log the error, delete the metadata
|
|
// and exit 0.
|
|
//
|
|
// `snapshot create --prune` runs the same purge, but cannot be driven
|
|
// this way: the refusal also breaks the backup's metadata export, which
|
|
// deletes the other snapshots from its copy of the local index.
|
|
//
|
|
//nolint:paralleltest // replaces os.Args, os.Stdout, os.Stderr and the xdg globals
|
|
func TestEntrySnapshotPurgeKeepsMetadataWhenLocalIndexDeleteFails(t *testing.T) {
|
|
configPath := writeHermeticPruneConfig(t, false)
|
|
dir := filepath.Dir(configPath)
|
|
indexPath := filepath.Join(dir, "index.sqlite")
|
|
|
|
// seedStaleSnapshotRecord writes an old snapshot to the local index
|
|
// only; the manifest planted here puts it on the destination store
|
|
// too, so the purge selects it rather than dropping it as stale.
|
|
seedStaleSnapshotRecord(t, indexPath)
|
|
|
|
// While the local index holds a snapshot, purge fails on a
|
|
// destination that answers that it has no marker.
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(dir, "store", "vaultik-destination.txt"), nil, 0o600))
|
|
|
|
manifestDir := filepath.Join(dir, "store", "metadata",
|
|
snapshot.RemoteSnapshotKey(stalePruneSnapshotID))
|
|
require.NoError(t, os.MkdirAll(manifestDir, 0o750))
|
|
|
|
manifestPath := filepath.Join(manifestDir, "manifest.json.zst")
|
|
require.NoError(t, os.WriteFile(manifestPath, []byte("stub"), 0o600))
|
|
|
|
ctx := context.Background()
|
|
|
|
db, err := database.New(ctx, indexPath)
|
|
require.NoError(t, err)
|
|
|
|
_, err = 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.NoError(t, db.Close())
|
|
|
|
code, _, stderr := runEntry(t, flagConfig, configPath,
|
|
cmdSnapshot, cmdPurge, flagOlderThan, "30d", flagForce)
|
|
|
|
assert.Equal(t, 1, code)
|
|
assert.Contains(t, stderr, "simulated index error",
|
|
"the purge must fail at the local-index delete")
|
|
assert.FileExists(t, manifestPath,
|
|
"the snapshot is still in the local index, so its metadata must stay")
|
|
}
|