Author SHA1 Message Date
sneak edc7131690 Correct two false doc comments (closes #403)
check / check (push) Failing after 1m27s
ExportSnapshotMetadata now lists the steps it performs and the metadata/ keys it writes. EnsureStorageBinding names `vaultik database delete`.

Comments only.

Model: opus-5-5
2026-11-09 06:18:48 +00:00
6 changed files with 44 additions and 178 deletions

No files matched your search

-3
View File
@@ -446,9 +446,6 @@ once the store is reachable to remove the snapshot's metadata from it
(`vaultik prune` does not). For a snapshot that is only on the (`vaultik prune` does not). For a snapshot that is only on the
destination store, that failure is an error. To wipe everything on the destination store, that failure is an error. To wipe everything on the
destination in one go, use `vaultik remote nuke --force`. destination in one go, use `vaultik remote nuke --force`.
* Accepts a snapshot ID, or — for a snapshot only on the destination
store — its remote key (or an unambiguous leading part of it) as shown
by `snapshot list`.
* `--local-only`: Skip remote cleanup; only touch the local index * `--local-only`: Skip remote cleanup; only touch the local index
* `--dry-run`: Show what would be deleted without deleting * `--dry-run`: Show what would be deleted without deleting
* `--force`: Skip confirmation prompt * `--force`: Skip confirmation prompt
-4
View File
@@ -288,10 +288,6 @@ know about), which is what 'vaultik prune' does. The command prints the
'vaultik prune' invocation to run as a follow-up only when it removed the 'vaultik prune' invocation to run as a follow-up only when it removed the
snapshot's metadata from the destination store, and never under --json. snapshot's metadata from the destination store, and never under --json.
The snapshot may be named by its ID or, on a host with no local index, by
the remote key that 'snapshot list' prints for a remote-only snapshot (an
unambiguous leading part is enough).
An ID that names no snapshot in the local index or on the destination An ID that names no snapshot in the local index or on the destination
store is an error. store is an error.
+7 -5
View File
@@ -271,16 +271,18 @@ func (sm *SnapshotManager) CompleteSnapshot(
return sm.MarkSnapshotComplete(ctx, snapshotID) return sm.MarkSnapshotComplete(ctx, snapshotID)
} }
// ExportSnapshotMetadata exports snapshot metadata to S3 // ExportSnapshotMetadata exports snapshot metadata to the backup destination store
// //
// This method executes the complete snapshot metadata export process: // This method executes the complete snapshot metadata export process:
// 1. Creates a temporary directory for working files // 1. Creates a temporary directory for working files
// 2. Copies the main database to preserve its state // 2. Copies the main database to preserve its state
// 3. Cleans the copy to contain only current snapshot data // 3. Cleans the copy to contain only current snapshot data
// 4. Dumps the cleaned database to SQL // 4. Vacuums the copy to remove the deleted data
// 5. Compresses the SQL dump with zstd // 5. Compresses the copy with zstd
// 6. Encrypts the compressed data with age (always; recipients are required) // 6. Encrypts the compressed copy with age (always; recipients are required)
// 7. Uploads to S3 at: snapshots/{snapshot-id}.sql.zst[.age] // 7. Builds the blob manifest from the copy, compressed but not encrypted
// 8. Uploads the database to metadata/{remote-key}/db.zst.age
// 9. Uploads the manifest to metadata/{remote-key}/manifest.json.zst
// //
// The only caller (finalizeSnapshotMetadata) does not close the main database // The only caller (finalizeSnapshotMetadata) does not close the main database
// before calling this method: the index is copied at dbPath while it is still // before calling this method: the index is copied at dbPath while it is still
-94
View File
@@ -451,100 +451,6 @@ func TestRemoveSnapshot_NotInLocalIndex(t *testing.T) {
assert.Contains(t, out, "Removed snapshot metadata from remote storage") assert.Contains(t, out, "Removed snapshot metadata from remote storage")
} }
// TestRemoveSnapshot_RemoteOnlyByRemoteKey checks that a snapshot only
// on the destination store, on a host with no local index, is removed by
// the remote key that 'snapshot list' prints for such a snapshot, and by
// an unambiguous leading part of it — the same identifiers verify and
// restore accept. Snapshot remove used to hash the argument again, so
// the hashed key never matched and only the plain snapshot ID worked.
func TestRemoveSnapshot_RemoteOnlyByRemoteKey(t *testing.T) {
log.Initialize(log.Config{})
t.Parallel()
remoteKey := snapshot.RemoteSnapshotKey("snapshot-001")
identifiers := map[string]string{
"full key": remoteKey,
"leading part": remoteKey[:8],
}
for name, identifier := range identifiers {
t.Run(name, func(t *testing.T) {
t.Parallel()
store := newTestStorer()
addManifest(t, store, "snapshot-001", nil)
// No local index: a host that sees the snapshot only remotely.
tv := vaultik.NewForTesting(store)
tv.Repositories = newLocalIndex(t)
result, err := tv.RemoveSnapshot(identifier,
&vaultik.RemoveOptions{Force: true})
require.NoError(t, err)
assert.True(t, result.RemoteRemoved)
assert.False(t, store.hasKey(remoteKeyPath("snapshot-001")))
out := tv.Stdout.String()
assert.NotContains(t, out, "from local database")
assert.Contains(t, out, "Removed snapshot metadata from remote storage")
})
}
}
// TestRemoveSnapshot_LocalIndexByRemoteKey checks that a remote key (or
// its unambiguous leading part) that names a snapshot the local index
// holds removes that snapshot's local row and its remote metadata
// together. `snapshot list --json` prints a remote key for every
// snapshot, including local ones.
func TestRemoveSnapshot_LocalIndexByRemoteKey(t *testing.T) {
log.Initialize(log.Config{})
t.Parallel()
remoteKey := snapshot.RemoteSnapshotKey("snapshot-001")
identifiers := map[string]string{
"full key": remoteKey,
"leading part": remoteKey[:8],
}
for name, identifier := range identifiers {
t.Run(name, func(t *testing.T) {
t.Parallel()
store := newTestStorer()
addManifest(t, store, "snapshot-001", nil)
// A backup wrote to this destination, so it carries the
// marker, and the local index holds the snapshot.
require.NoError(t, store.Put(context.Background(), markerFile,
bytes.NewReader(nil)))
tv := vaultik.NewForTesting(store)
repos := newLocalIndex(t, "snapshot-001")
tv.Repositories = repos
result, err := tv.RemoveSnapshot(identifier,
&vaultik.RemoveOptions{Force: true})
require.NoError(t, err)
assert.True(t, result.RemoteRemoved)
assert.False(t, store.hasKey(remoteKeyPath("snapshot-001")))
row, err := repos.Snapshots.GetByID(
context.Background(), "snapshot-001")
require.NoError(t, err)
assert.Nil(t, row)
out := tv.Stdout.String()
assert.Contains(t, out,
"Removed snapshot 'snapshot-001' from local database")
assert.Contains(t, out, "Removed snapshot metadata from remote storage")
})
}
}
// TestRemoveSnapshot_NotInLocalIndexDeleteFails checks that removing a // TestRemoveSnapshot_NotInLocalIndexDeleteFails checks that removing a
// snapshot only the destination store holds fails when its metadata // snapshot only the destination store holds fails when its metadata
// cannot be deleted there, under --force and under --json: nothing was // cannot be deleted there, under --force and under --json: nothing was
+36 -71
View File
@@ -1480,7 +1480,7 @@ func (v *Vaultik) RemoveSnapshot(
return result, err return result, err
} }
inLocalIndex, localID, remoteKey, err := v.findSnapshotToRemove(snapshotID, opts) inLocalIndex, err := v.findSnapshotToRemove(snapshotID, opts)
if err != nil { if err != nil {
return result, err return result, err
} }
@@ -1502,16 +1502,16 @@ func (v *Vaultik) RemoveSnapshot(
} }
if inLocalIndex { if inLocalIndex {
log.Info("Removing snapshot from local database", "snapshot_id", localID) log.Info("Removing snapshot from local database", "snapshot_id", snapshotID)
err = v.deleteSnapshotFromLocalDB(localID) err = v.deleteSnapshotFromLocalDB(snapshotID)
if err != nil { if err != nil {
return result, fmt.Errorf("removing from local database: %w", err) return result, fmt.Errorf("removing from local database: %w", err)
} }
} }
result.RemoteRemoved, err = v.removeSnapshotRemote( result.RemoteRemoved, err = v.removeSnapshotRemote(
snapshotID, remoteKey, inLocalIndex, markerErr, opts) snapshotID, inLocalIndex, markerErr, opts)
if err != nil { if err != nil {
return result, err return result, err
} }
@@ -1530,7 +1530,7 @@ func (v *Vaultik) RemoveSnapshot(
if inLocalIndex { if inLocalIndex {
v.UI.Infof("Removed snapshot '%s' from local database.", v.UI.Infof("Removed snapshot '%s' from local database.",
v.UI.Snapshot(localID)) v.UI.Snapshot(snapshotID))
} }
if result.RemoteRemoved { if result.RemoteRemoved {
@@ -1543,68 +1543,50 @@ func (v *Vaultik) RemoveSnapshot(
return result, nil return result, nil
} }
// findSnapshotToRemove resolves snapshotID, the identifier remove was // findSnapshotToRemove reports whether the local index has a row for
// given, and returns whether the local index holds that snapshot, the // snapshotID, and returns errSnapshotNotFound when there is nothing to
// human ID of its row when it does, and the remote key to strip from // remove: no row and, unless LocalOnly is set, no metadata on the
// the destination store. A human ID is looked up in the local index // destination store either. The destination store is consulted only
// literally and hashed without consulting the store; a remote key, or // when the local index has no row, so a snapshot in the local index can
// an unambiguous leading part of one, is resolved against the store's // still be removed from it while the destination store is unreachable,
// metadata listing the way verify and restore do, and then matched back // and only after checkDestinationMarker passes, so an unplugged volume's
// to the local row whose hashed ID equals it, so removing by such a key // empty mount point is not taken for a store without the snapshot.
// removes the local row too. The destination store is consulted only
// after checkDestinationMarker, so an unplugged volume's empty mount
// point is not taken for a store without the snapshot. The identifier
// naming nothing, on the local index or, unless LocalOnly is set, on
// the store, is errSnapshotNotFound; a remote-key form that matches
// nothing or more than one directory surfaces
// resolveSnapshotRemoteKey's errSnapshotKeyNotFound or
// errSnapshotKeyAmbiguous.
func (v *Vaultik) findSnapshotToRemove( func (v *Vaultik) findSnapshotToRemove(
snapshotID string, opts *RemoveOptions, snapshotID string, opts *RemoveOptions,
) (bool, string, string, error) { ) (bool, error) {
if v.Repositories != nil { if v.Repositories != nil {
snap, err := v.Repositories.Snapshots.GetByID(v.ctx, snapshotID) snap, err := v.Repositories.Snapshots.GetByID(v.ctx, snapshotID)
if err != nil { if err != nil {
return false, "", "", fmt.Errorf("looking up snapshot in local index: %w", err) return false, fmt.Errorf("looking up snapshot in local index: %w", err)
} }
if snap != nil { if snap != nil {
return true, snapshotID, snapshot.RemoteSnapshotKey(snapshotID), nil return true, nil
} }
} }
if opts.LocalOnly { if opts.LocalOnly {
return false, "", "", fmt.Errorf("%w in the local index: %s", return false, fmt.Errorf("%w in the local index: %s",
errSnapshotNotFound, snapshotID) errSnapshotNotFound, snapshotID)
} }
err := v.checkDestinationMarker() err := v.checkDestinationMarker()
if err != nil { if err != nil {
return false, "", "", err return false, err
} }
remoteKey, err := v.resolveSnapshotRemoteKey(snapshotID) objects, err := v.remoteSnapshotObjects(snapshot.RemoteSnapshotKey(snapshotID))
if err != nil { if err != nil {
return false, "", "", err return false, fmt.Errorf("looking up snapshot on destination store: %w", err)
}
objects, err := v.remoteSnapshotObjects(remoteKey)
if err != nil {
return false, "", "", fmt.Errorf("looking up snapshot on destination store: %w", err)
} }
if len(objects) == 0 { if len(objects) == 0 {
return false, "", "", fmt.Errorf( return false, fmt.Errorf(
"%w in the local index or on the destination store: %s", "%w in the local index or on the destination store: %s",
errSnapshotNotFound, snapshotID) errSnapshotNotFound, snapshotID)
} }
localID, err := v.localSnapshotIDForRemoteKey(remoteKey) return false, nil
if err != nil {
return false, "", "", err
}
return localID != "", localID, remoteKey, nil
} }
// removeSnapshotDryRun reports what RemoveSnapshot would do without // removeSnapshotDryRun reports what RemoveSnapshot would do without
@@ -1667,20 +1649,19 @@ func (v *Vaultik) confirmRemoveSnapshot(snapshotID string, opts *RemoveOptions)
} }
// removeSnapshotRemote strips the snapshot's metadata from the // removeSnapshotRemote strips the snapshot's metadata from the
// destination store. remoteKey is the resolved metadata directory name // destination store. It returns true only when the delete finished
// to delete, as returned by findSnapshotToRemove. It returns true only // without an error and removed at least one object; under LocalOnly it
// when the delete finished without an error and removed at least one // does nothing. markerErr is the result of checkDestinationMarker; when
// object; under LocalOnly it does nothing. markerErr is the result of // it is set, nothing is deleted and it is the failure. When the snapshot
// checkDestinationMarker; when it is set, nothing is deleted and it is // is only on the destination store, a failure is returned as an error,
// the failure. When the snapshot is only on the destination store, a // because that removal is all the command does. When the snapshot was in
// failure is returned as an error, because that removal is all the // the local index, its removal there has already happened, so a failure
// command does. When the snapshot was in the local index, its removal // is only a warning telling the user to run `vaultik snapshot remove`
// there has already happened, so a failure is only a warning telling // for the snapshot again once the destination store is reachable
// the user to run `vaultik snapshot remove` for the snapshot again once // (`vaultik prune` never removes snapshot metadata that has its
// the destination store is reachable (`vaultik prune` never removes // manifest).
// snapshot metadata that has its manifest).
func (v *Vaultik) removeSnapshotRemote( func (v *Vaultik) removeSnapshotRemote(
snapshotID, remoteKey string, inLocalIndex bool, markerErr error, snapshotID string, inLocalIndex bool, markerErr error,
opts *RemoveOptions, opts *RemoveOptions,
) (bool, error) { ) (bool, error) {
if opts.LocalOnly { if opts.LocalOnly {
@@ -1694,7 +1675,8 @@ func (v *Vaultik) removeSnapshotRemote(
err := markerErr err := markerErr
if err == nil { if err == nil {
deleted, err = v.deleteRemoteSnapshotByKey(remoteKey) deleted, err = v.deleteRemoteSnapshotByKey(
snapshot.RemoteSnapshotKey(snapshotID))
} }
if err != nil && !inLocalIndex { if err != nil && !inLocalIndex {
@@ -1808,23 +1790,6 @@ func (v *Vaultik) localSnapshotIDs() ([]string, error) {
return ids, nil return ids, nil
} }
// localSnapshotIDForRemoteKey returns the human snapshot ID of the local
// index row whose RemoteSnapshotKey is key, or "" when no row has it.
func (v *Vaultik) localSnapshotIDForRemoteKey(key string) (string, error) {
ids, err := v.localSnapshotIDs()
if err != nil {
return "", fmt.Errorf("listing local snapshots: %w", err)
}
for _, id := range ids {
if snapshot.RemoteSnapshotKey(id) == key {
return id, nil
}
}
return "", nil
}
// listAllRemoteSnapshotKeys collects the hashed remote keys // listAllRemoteSnapshotKeys collects the hashed remote keys
// (subdirectories under metadata/) currently present in the // (subdirectories under metadata/) currently present in the
// destination store. Returns (nil, err) when the store cannot be // destination store. Returns (nil, err) when the store cannot be
+1 -1
View File
@@ -30,7 +30,7 @@ var errStorageBindingMismatch = errors.New(
// nothing and return nil. // nothing and return nil.
// - When the two differ, refuse with an error that tells the user // - When the two differ, refuse with an error that tells the user
// how to recover (revert the config, or run `vaultik database // how to recover (revert the config, or run `vaultik database
// purge` to discard the local index and rebuild against the new // delete` to discard the local index and rebuild against the new
// destination on the next backup). // destination on the next backup).
// //
// Read-only inspection commands (remote info, snapshot list, etc.) // Read-only inspection commands (remote info, snapshot list, etc.)