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
104 lines
3.5 KiB
Go
104 lines
3.5 KiB
Go
package vaultik
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"sneak.berlin/go/vaultik/internal/log"
|
|
"sneak.berlin/go/vaultik/internal/storage"
|
|
)
|
|
|
|
// destinationMarkerKey is the object every backup writes at the root of
|
|
// the backup destination store. While the local index holds a snapshot
|
|
// stored there, a destination that answers a look-up of it with not
|
|
// found is treated as not present: an unplugged volume leaves an empty
|
|
// directory at its mount point, which would otherwise read as a store
|
|
// whose snapshots are all gone.
|
|
const destinationMarkerKey = "vaultik-destination.txt"
|
|
|
|
// destinationMarkerText is the whole content of the marker. It names no
|
|
// host, path or time.
|
|
const destinationMarkerText = "This is a vaultik backup destination store.\n"
|
|
|
|
// errDestinationNotPresent is returned when the local index holds a
|
|
// snapshot stored on the backup destination store and the store answers
|
|
// that it has no marker.
|
|
var errDestinationNotPresent = errors.New("backup destination store is not present")
|
|
|
|
// checkDestinationMarker returns errDestinationNotPresent when the local
|
|
// index holds a snapshot stored on the backup destination store and the
|
|
// store answers that it has no marker. With no snapshot in the local
|
|
// index, as on a first backup or on another machine, there is nothing to
|
|
// check; nor when the local index is bound to a different destination
|
|
// than the configured one, since its snapshots are stored there.
|
|
//
|
|
// A look-up of the marker that fails for any other reason than not
|
|
// found is logged and otherwise ignored, because the check could not be
|
|
// made: S3 refuses it to credentials that allow only PUT, which is all
|
|
// a backup needs. The check exists for a file:// volume's empty mount
|
|
// point, which always answers not found, and a destination that cannot
|
|
// be reached fails on the next request made to it, such as the backup's
|
|
// PUT of the marker.
|
|
func (v *Vaultik) checkDestinationMarker() error {
|
|
ids, err := v.localSnapshotIDs()
|
|
if err != nil {
|
|
return fmt.Errorf("listing local snapshots: %w", err)
|
|
}
|
|
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Config is nil only in tests, which then record no binding (see
|
|
// EnsureStorageBinding).
|
|
if v.Config != nil {
|
|
stored, configured, err := v.storageBinding()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if stored != "" && stored != configured {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
_, err = v.Storage.Stat(v.ctx, destinationMarkerKey)
|
|
if errors.Is(err, storage.ErrNotFound) {
|
|
return fmt.Errorf("%w: %s has no %s, but the local index holds "+
|
|
"snapshots stored there; mount the volume if it is removable, "+
|
|
"or if the destination was emptied on purpose, run "+
|
|
"'vaultik database delete' and back up again",
|
|
errDestinationNotPresent, v.Storage.Info().Location,
|
|
destinationMarkerKey)
|
|
}
|
|
|
|
// Info, not a warning: --cron must stay silent on a successful
|
|
// backup from a host whose credentials allow only PUT.
|
|
if err != nil {
|
|
log.Info("Could not check the backup destination store for its marker",
|
|
"key", destinationMarkerKey, "error", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// markDestination checks the backup destination store with
|
|
// checkDestinationMarker and then writes the marker, so a first backup
|
|
// marks its destination and a later one writes the same text again.
|
|
func (v *Vaultik) markDestination() error {
|
|
err := v.checkDestinationMarker()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = v.Storage.Put(v.ctx, destinationMarkerKey,
|
|
strings.NewReader(destinationMarkerText))
|
|
if err != nil {
|
|
return fmt.Errorf("writing %s to the backup destination store: %w",
|
|
destinationMarkerKey, err)
|
|
}
|
|
|
|
return nil
|
|
}
|