Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42c4bb325 |
No files matched your search
@@ -32,8 +32,8 @@ mkdir /Volumes/usbstick/mybackup
|
|||||||
# (System Settings → Privacy & Security → Full Disk Access), otherwise
|
# (System Settings → Privacy & Security → Full Disk Access), otherwise
|
||||||
# the backup will abort with a permission error on protected directories
|
# the backup will abort with a permission error on protected directories
|
||||||
|
|
||||||
# run your first backup (the default config backs up ~, and /Applications
|
# run your first backup (the default config backs up ~ and /Applications
|
||||||
# on macOS only, with sensible excludes)
|
# with sensible excludes)
|
||||||
vaultik snapshot create
|
vaultik snapshot create
|
||||||
|
|
||||||
# see what you have
|
# see what you have
|
||||||
|
|||||||
@@ -24,11 +24,6 @@ owner's; a `v*` tag runs the release build in
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-11-10: Made `config init` write the `apps` snapshot, which backs
|
|
||||||
up `/Applications`, only on macOS
|
|
||||||
([issue #408](https://git.eeqj.de/sneak/vaultik/issues/408)). On
|
|
||||||
Linux the first `snapshot create` stopped on the missing directory.
|
|
||||||
|
|
||||||
- 2026-10-15: Stopped `snapshot create --prune` pruning after a backup
|
- 2026-10-15: Stopped `snapshot create --prune` pruning after a backup
|
||||||
in which a volume mounted below a configured path in an earlier
|
in which a volume mounted below a configured path in an earlier
|
||||||
snapshot of the same name was not mounted
|
snapshot of the same name was not mounted
|
||||||
|
|||||||
+1
-1
@@ -90,7 +90,7 @@ func setupGlobals(
|
|||||||
// blank line. Used both from the fx hook (for subcommand invocations) and
|
// blank line. Used both from the fx hook (for subcommand invocations) and
|
||||||
// from the root cobra Run handler (for `vaultik` with no subcommand).
|
// from the root cobra Run handler (for `vaultik` with no subcommand).
|
||||||
func writeStartupBanner(w *ui.Writer, startTime time.Time, shortCommit string) {
|
func writeStartupBanner(w *ui.Writer, startTime time.Time, shortCommit string) {
|
||||||
w.Bannerf("%s %s by %s (commit %s, commit date %s) starting up at %s.",
|
w.Bannerf("%s %s by %s (commit %s, built on %s) starting up at %s.",
|
||||||
globals.Appname, globals.Version, globals.Author,
|
globals.Appname, globals.Version, globals.Author,
|
||||||
shortCommit, globals.CommitDate,
|
shortCommit, globals.CommitDate,
|
||||||
startTime.Format(time.RFC3339))
|
startTime.Format(time.RFC3339))
|
||||||
|
|||||||
+4
-27
@@ -8,7 +8,6 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -31,7 +30,7 @@ const configSetArgs = 2
|
|||||||
// parent config dirs (e.g. ~/.config) are conventionally traversable.
|
// parent config dirs (e.g. ~/.config) are conventionally traversable.
|
||||||
const configDirMode = 0o755
|
const configDirMode = 0o755
|
||||||
|
|
||||||
// configYAMLIndent matches the 2-space indentation of the default config,
|
// configYAMLIndent matches the 2-space indentation of defaultConfigTemplate,
|
||||||
// so `config set` writes the file back with the same indentation rather than
|
// so `config set` writes the file back with the same indentation rather than
|
||||||
// yaml.Marshal's 4-space default.
|
// yaml.Marshal's 4-space default.
|
||||||
const configYAMLIndent = 2
|
const configYAMLIndent = 2
|
||||||
@@ -51,22 +50,7 @@ var (
|
|||||||
"so the file was not changed")
|
"so the file was not changed")
|
||||||
)
|
)
|
||||||
|
|
||||||
// macOS is the value of runtime.GOOS on macOS.
|
const defaultConfigTemplate = `# vaultik configuration
|
||||||
const macOS = "darwin"
|
|
||||||
|
|
||||||
// defaultConfigText returns the file `config init` writes on the operating
|
|
||||||
// system goos, named as runtime.GOOS names it. Only macOS gets the apps
|
|
||||||
// snapshot, since /Applications exists nowhere else.
|
|
||||||
func defaultConfigText(goos string) string {
|
|
||||||
if goos == macOS {
|
|
||||||
return defaultConfigHead + appsSnapshotConfig + defaultConfigTail
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultConfigHead + defaultConfigTail
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultConfigHead is the default config up to the end of the home snapshot.
|
|
||||||
const defaultConfigHead = `# vaultik configuration
|
|
||||||
# Documentation: https://sneak.berlin/go/vaultik
|
# Documentation: https://sneak.berlin/go/vaultik
|
||||||
|
|
||||||
# ─── REQUIRED ────────────────────────────────────────────────────────────────
|
# ─── REQUIRED ────────────────────────────────────────────────────────────────
|
||||||
@@ -184,10 +168,7 @@ snapshots:
|
|||||||
- "/Movies/ProxyMedia"
|
- "/Movies/ProxyMedia"
|
||||||
- "/Music/iTunes/Album Artwork"
|
- "/Music/iTunes/Album Artwork"
|
||||||
- "/Pictures/iPod Photo Cache"
|
- "/Pictures/iPod Photo Cache"
|
||||||
`
|
|
||||||
|
|
||||||
// appsSnapshotConfig is the apps snapshot of the default config.
|
|
||||||
const appsSnapshotConfig = `
|
|
||||||
# Third-party applications. OS-provided apps live in /System/Applications
|
# Third-party applications. OS-provided apps live in /System/Applications
|
||||||
# on modern macOS and are never in /Applications, but Apple-installed
|
# on modern macOS and are never in /Applications, but Apple-installed
|
||||||
# App Store apps (Safari, GarageBand, iWork, iMovie) are excluded since
|
# App Store apps (Safari, GarageBand, iWork, iMovie) are excluded since
|
||||||
@@ -208,10 +189,7 @@ const appsSnapshotConfig = `
|
|||||||
- "/Steam.app"
|
- "/Steam.app"
|
||||||
- "/VirtualBox.app"
|
- "/VirtualBox.app"
|
||||||
- "/Utilities/Adobe Installers"
|
- "/Utilities/Adobe Installers"
|
||||||
`
|
|
||||||
|
|
||||||
// defaultConfigTail is the default config after the snapshots.
|
|
||||||
const defaultConfigTail = `
|
|
||||||
# Storage backend (pick ONE of the three forms below).
|
# Storage backend (pick ONE of the three forms below).
|
||||||
#
|
#
|
||||||
# S3-compatible:
|
# S3-compatible:
|
||||||
@@ -333,8 +311,7 @@ on macOS, ~/.config/ on Linux, /etc/vaultik/ as root).`,
|
|||||||
return fmt.Errorf("creating config directory %s: %w", dir, err)
|
return fmt.Errorf("creating config directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.WriteFile(path, []byte(defaultConfigText(runtime.GOOS)),
|
err = os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
|
||||||
configFileMode)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("writing config file: %w", err)
|
return fmt.Errorf("writing config file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -583,7 +560,7 @@ func yamlPathOnly(root *yaml.Node, keys []string) (*yaml.Node, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// marshalConfigYAML renders a config document tree with 2-space indentation,
|
// marshalConfigYAML renders a config document tree with 2-space indentation,
|
||||||
// matching the default config. yaml.Marshal defaults to 4 spaces, which
|
// matching defaultConfigTemplate. yaml.Marshal defaults to 4 spaces, which
|
||||||
// would reindent the whole file on the first `config set` despite the promise
|
// would reindent the whole file on the first `config set` despite the promise
|
||||||
// to preserve formatting.
|
// to preserve formatting.
|
||||||
func marshalConfigYAML(root *yaml.Node) ([]byte, error) {
|
func marshalConfigYAML(root *yaml.Node) ([]byte, error) {
|
||||||
|
|||||||
@@ -13,15 +13,14 @@ import (
|
|||||||
"sneak.berlin/go/vaultik/internal/ui"
|
"sneak.berlin/go/vaultik/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestDefaultConfigParsesOnMacOS ensures the config `config init` writes on
|
// TestDefaultConfigTemplateParses ensures the init template is valid YAML
|
||||||
// macOS is valid YAML that unmarshals into the Config struct with the
|
// that unmarshals into the Config struct with the expected snapshots.
|
||||||
// expected snapshots.
|
func TestDefaultConfigTemplateParses(t *testing.T) {
|
||||||
func TestDefaultConfigParsesOnMacOS(t *testing.T) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
var cfg config.Config
|
var cfg config.Config
|
||||||
|
|
||||||
err := yaml.Unmarshal([]byte(defaultConfigText("darwin")), &cfg)
|
err := yaml.Unmarshal([]byte(defaultConfigTemplate), &cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("default config template is not valid YAML: %v", err)
|
t.Fatalf("default config template is not valid YAML: %v", err)
|
||||||
}
|
}
|
||||||
@@ -59,39 +58,6 @@ func TestDefaultConfigParsesOnMacOS(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDefaultConfigOnLinux checks that the config `config init` writes on
|
|
||||||
// Linux loads with its home snapshot and without the apps snapshot, whose
|
|
||||||
// /Applications exists only on macOS.
|
|
||||||
func TestDefaultConfigOnLinux(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "config.yml")
|
|
||||||
|
|
||||||
err := os.WriteFile(path, []byte(defaultConfigText("linux")), configFileMode)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("write config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = writeConfigSet(ui.NewWithColor(&bytes.Buffer{}, false),
|
|
||||||
path, "storage_url", "file:///mnt/backups")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("config set storage_url: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := config.Load(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("config.Load: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := cfg.Snapshots["home"]; !ok {
|
|
||||||
t.Error("expected 'home' snapshot in the Linux default config")
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := cfg.Snapshots["apps"]; ok {
|
|
||||||
t.Error("the Linux default config should have no 'apps' snapshot")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// testRecipient is a valid age public key.
|
// testRecipient is a valid age public key.
|
||||||
const testRecipient = "age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj"
|
const testRecipient = "age1278m9q7dp3chsh2dcy82qk27v047zywyvtxwnj4cvt0z65jw6a7q5dqhfj"
|
||||||
|
|
||||||
@@ -105,7 +71,7 @@ func TestConfigSetRecipientOnFreshConfig(t *testing.T) {
|
|||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "config.yml")
|
path := filepath.Join(t.TempDir(), "config.yml")
|
||||||
|
|
||||||
err := os.WriteFile(path, []byte(defaultConfigText(macOS)), configFileMode)
|
err := os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("write config: %v", err)
|
t.Fatalf("write config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -254,7 +220,7 @@ func TestConfigSetKeepsFileThatWouldNotLoad(t *testing.T) {
|
|||||||
{levelReference, "blob_size_limit", "1MB",
|
{levelReference, "blob_size_limit", "1MB",
|
||||||
"blob_size_limit must be at least"},
|
"blob_size_limit must be at least"},
|
||||||
// The type check on a config that does not load yet.
|
// The type check on a config that does not load yet.
|
||||||
{defaultConfigText(macOS), "snapshots.apps.paths", "/srv",
|
{defaultConfigTemplate, "snapshots.apps.paths", "/srv",
|
||||||
"into []string"},
|
"into []string"},
|
||||||
// The type check on a config that cannot be read here.
|
// The type check on a config that cannot be read here.
|
||||||
{configWithUnsetReference, "snapshots.home.paths", "/x",
|
{configWithUnsetReference, "snapshots.home.paths", "/x",
|
||||||
@@ -414,7 +380,7 @@ func loadAfterConfigSet(t *testing.T, key, value string) *config.Config {
|
|||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "config.yml")
|
path := filepath.Join(t.TempDir(), "config.yml")
|
||||||
|
|
||||||
err := os.WriteFile(path, []byte(defaultConfigText(macOS)), configFileMode)
|
err := os.WriteFile(path, []byte(defaultConfigTemplate), configFileMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("write config: %v", err)
|
t.Fatalf("write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/adrg/xdg"
|
"github.com/adrg/xdg"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"sneak.berlin/go/vaultik/internal/globals"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Command words and flags used to build argument vectors below. They are
|
// Command words and flags used to build argument vectors below. They are
|
||||||
@@ -113,35 +112,6 @@ func TestBannerStillPrintedWithoutSuppressingFlag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestBannerLabelsCommitDate checks that the banner prints the commit
|
|
||||||
// and the commit date, each after its own label. Commit and CommitDate
|
|
||||||
// get different values, so printing the wrong one fails.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // sets globals.Commit and globals.CommitDate
|
|
||||||
func TestBannerLabelsCommitDate(t *testing.T) {
|
|
||||||
const (
|
|
||||||
commit = "abc1234"
|
|
||||||
commitDate = "2026-11-09"
|
|
||||||
)
|
|
||||||
|
|
||||||
previousCommit, previousCommitDate := globals.Commit, globals.CommitDate
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
globals.Commit = previousCommit
|
|
||||||
globals.CommitDate = previousCommitDate
|
|
||||||
})
|
|
||||||
|
|
||||||
globals.Commit = commit
|
|
||||||
globals.CommitDate = commitDate
|
|
||||||
|
|
||||||
var banner bytes.Buffer
|
|
||||||
|
|
||||||
emitStartupBanner(nil, &banner)
|
|
||||||
|
|
||||||
assert.Contains(t, banner.String(),
|
|
||||||
"(commit "+commit+", commit date "+commitDate+")")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestBannerSuppressedInArgs covers the suppression scan directly,
|
// TestBannerSuppressedInArgs covers the suppression scan directly,
|
||||||
// including the flags that suppressed the banner before --json joined
|
// including the flags that suppressed the banner before --json joined
|
||||||
// them, so that adding --json cannot regress them.
|
// them, so that adding --json cannot regress them.
|
||||||
|
|||||||
@@ -37,13 +37,13 @@ func writeVersion(out *ui.Writer) {
|
|||||||
w := out.Out()
|
w := out.Out()
|
||||||
|
|
||||||
_, _ = fmt.Fprintf(w, "vaultik %s\n", globals.Version)
|
_, _ = fmt.Fprintf(w, "vaultik %s\n", globals.Version)
|
||||||
_, _ = fmt.Fprintf(w, " commit: %s\n", globals.Commit)
|
_, _ = fmt.Fprintf(w, " commit: %s\n", globals.Commit)
|
||||||
_, _ = fmt.Fprintf(w, " commit date: %s\n", globals.CommitDate)
|
_, _ = fmt.Fprintf(w, " build date: %s\n", globals.CommitDate)
|
||||||
_, _ = fmt.Fprintf(w, " go: %s\n", runtime.Version())
|
_, _ = fmt.Fprintf(w, " go: %s\n", runtime.Version())
|
||||||
_, _ = fmt.Fprintf(w, " os/arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
_, _ = fmt.Fprintf(w, " os/arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||||
_, _ = fmt.Fprintf(w, " author: %s\n", globals.Author)
|
_, _ = fmt.Fprintf(w, " author: %s\n", globals.Author)
|
||||||
_, _ = fmt.Fprintf(w, " homepage: %s\n", globals.Homepage)
|
_, _ = fmt.Fprintf(w, " homepage: %s\n", globals.Homepage)
|
||||||
_, _ = fmt.Fprintf(w, " license: %s\n", globals.License)
|
_, _ = fmt.Fprintf(w, " license: %s\n", globals.License)
|
||||||
|
|
||||||
if globals.IsDevVersion(globals.Version) {
|
if globals.IsDevVersion(globals.Version) {
|
||||||
_, _ = fmt.Fprintln(w)
|
_, _ = fmt.Fprintln(w)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package cli_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -50,42 +49,6 @@ func TestVersionCommandReportsBuildVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestVersionCommandReportLines checks the label, padding and value of
|
|
||||||
// every line of the report. Commit and CommitDate get different values,
|
|
||||||
// so a line that prints the wrong one fails.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // sets globals.Commit and globals.CommitDate
|
|
||||||
func TestVersionCommandReportLines(t *testing.T) {
|
|
||||||
const (
|
|
||||||
commit = "abc1234"
|
|
||||||
commitDate = "2026-11-09"
|
|
||||||
)
|
|
||||||
|
|
||||||
previousCommit, previousCommitDate := globals.Commit, globals.CommitDate
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
globals.Commit = previousCommit
|
|
||||||
globals.CommitDate = previousCommitDate
|
|
||||||
})
|
|
||||||
|
|
||||||
globals.Commit = commit
|
|
||||||
globals.CommitDate = commitDate
|
|
||||||
|
|
||||||
want := "vaultik " + globals.Version + "\n" +
|
|
||||||
" commit: " + commit + "\n" +
|
|
||||||
" commit date: " + commitDate + "\n" +
|
|
||||||
" go: " + runtime.Version() + "\n" +
|
|
||||||
" os/arch: " + runtime.GOOS + "/" + runtime.GOARCH + "\n" +
|
|
||||||
" author: " + globals.Author + "\n" +
|
|
||||||
" homepage: " + globals.Homepage + "\n" +
|
|
||||||
" license: " + globals.License + "\n"
|
|
||||||
|
|
||||||
out := runVersionCommand(t)
|
|
||||||
if !strings.HasPrefix(out, want) {
|
|
||||||
t.Errorf("report:\n%s\nwant it to start with:\n%s", out, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestVersionCommandFlagsDevelopmentBuild is the regression test for
|
// TestVersionCommandFlagsDevelopmentBuild is the regression test for
|
||||||
// the thing this command exists to prevent: a build that is not a
|
// the thing this command exists to prevent: a build that is not a
|
||||||
// release must say so. The notice used to be gated on the version
|
// release must say so. The notice used to be gated on the version
|
||||||
|
|||||||
@@ -271,18 +271,16 @@ func (sm *SnapshotManager) CompleteSnapshot(
|
|||||||
return sm.MarkSnapshotComplete(ctx, snapshotID)
|
return sm.MarkSnapshotComplete(ctx, snapshotID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportSnapshotMetadata exports snapshot metadata to the backup destination store
|
// ExportSnapshotMetadata exports snapshot metadata to S3
|
||||||
//
|
//
|
||||||
// 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. Vacuums the copy to remove the deleted data
|
// 4. Dumps the cleaned database to SQL
|
||||||
// 5. Compresses the copy with zstd
|
// 5. Compresses the SQL dump with zstd
|
||||||
// 6. Encrypts the compressed copy with age (always; recipients are required)
|
// 6. Encrypts the compressed data with age (always; recipients are required)
|
||||||
// 7. Builds the blob manifest from the copy, compressed but not encrypted
|
// 7. Uploads to S3 at: snapshots/{snapshot-id}.sql.zst[.age]
|
||||||
// 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
|
||||||
|
|||||||
@@ -31,10 +31,9 @@ var (
|
|||||||
errPathsNotInSnapshot = errors.New("requested path(s) not in the snapshot")
|
errPathsNotInSnapshot = errors.New("requested path(s) not in the snapshot")
|
||||||
errFilesFailedVerify = errors.New("files failed verification")
|
errFilesFailedVerify = errors.New("files failed verification")
|
||||||
errDecryptionKeyRequired = errors.New(
|
errDecryptionKeyRequired = errors.New(
|
||||||
"decryption key required for restore\n\n" +
|
"age secret key is missing; set it in the " +
|
||||||
"Set the VAULTIK_AGE_SECRET_KEY environment variable to your " +
|
"VAULTIK_AGE_SECRET_KEY environment variable or the " +
|
||||||
"age private key file:\n" +
|
"age_secret_key config file field")
|
||||||
" export VAULTIK_AGE_SECRET_KEY=\"$(cat vaultik_backup_private_key.txt)\"")
|
|
||||||
// errInvalidAgeSecretKey is returned when the configured key does not
|
// errInvalidAgeSecretKey is returned when the configured key does not
|
||||||
// parse as any age identity. It names the source but never the value,
|
// parse as any age identity. It names the source but never the value,
|
||||||
// which is secret, so the message is safe to print and log.
|
// which is secret, so the message is safe to print and log.
|
||||||
|
|||||||
@@ -106,3 +106,23 @@ func TestRestoreIdentitiesMissingKey(t *testing.T) {
|
|||||||
_, err := v.restoreIdentities()
|
_, err := v.restoreIdentities()
|
||||||
require.ErrorIs(t, err, errDecryptionKeyRequired)
|
require.ErrorIs(t, err, errDecryptionKeyRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMissingKeyErrorsNameBothSources asserts restore and deep verify
|
||||||
|
// return the same missing-key error, whose text names both places the key
|
||||||
|
// can be set: the VAULTIK_AGE_SECRET_KEY environment variable first since
|
||||||
|
// it is preferred, then the age_secret_key config file field.
|
||||||
|
func TestMissingKeyErrorsNameBothSources(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const want = "age secret key is missing; set it in " +
|
||||||
|
"the VAULTIK_AGE_SECRET_KEY environment variable or the " +
|
||||||
|
"age_secret_key config file field"
|
||||||
|
|
||||||
|
v := &Vaultik{Config: &config.Config{}}
|
||||||
|
|
||||||
|
_, err := v.restoreIdentities()
|
||||||
|
require.EqualError(t, err, want)
|
||||||
|
|
||||||
|
err = v.RunDeepVerify("any-snapshot", &VerifyOptions{})
|
||||||
|
require.EqualError(t, err, want)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
// delete` to discard the local index and rebuild against the new
|
// purge` 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.)
|
||||||
|
|||||||
@@ -162,11 +162,6 @@ func (v *Vaultik) StartOperation(fn func()) func(context.Context) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanDecrypt returns true if this Vaultik instance has decryption capabilities
|
|
||||||
func (v *Vaultik) CanDecrypt() bool {
|
|
||||||
return v.Config.AgeSecretKey != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFilesystem returns the filesystem instance used by Vaultik
|
// GetFilesystem returns the filesystem instance used by Vaultik
|
||||||
//
|
//
|
||||||
//nolint:ireturn // afero.Fs is the filesystem abstraction by design
|
//nolint:ireturn // afero.Fs is the filesystem abstraction by design
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ import (
|
|||||||
|
|
||||||
// Sentinel errors for snapshot verification failures.
|
// Sentinel errors for snapshot verification failures.
|
||||||
var (
|
var (
|
||||||
errVerificationFailed = errors.New("verification failed")
|
errVerificationFailed = errors.New("verification failed")
|
||||||
errSecretKeyRequired = errors.New(
|
|
||||||
"VAULTIK_AGE_SECRET_KEY not set; required for deep verification")
|
|
||||||
errChunksOutOfOrder = errors.New("chunks out of order")
|
errChunksOutOfOrder = errors.New("chunks out of order")
|
||||||
errChunkHashMismatch = errors.New("chunk hash mismatch")
|
errChunkHashMismatch = errors.New("chunk hash mismatch")
|
||||||
errNegativeChunkLength = errors.New("chunk length is negative")
|
errNegativeChunkLength = errors.New("chunk length is negative")
|
||||||
@@ -106,13 +104,9 @@ func (v *Vaultik) RunDeepVerify(snapshotID string, opts *VerifyOptions) error {
|
|||||||
Mode: "deep",
|
Mode: "deep",
|
||||||
}
|
}
|
||||||
|
|
||||||
if !v.CanDecrypt() {
|
|
||||||
return v.deepVerifyFailure(result, opts,
|
|
||||||
errSecretKeyRequired.Error(), errSecretKeyRequired)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse the age secret key once, the same way restore does, and reuse
|
// Parse the age secret key once, the same way restore does, and reuse
|
||||||
// the identities for the database and every blob.
|
// the identities for the database and every blob. A missing key fails
|
||||||
|
// here, before anything is downloaded.
|
||||||
identities, err := v.restoreIdentities()
|
identities, err := v.restoreIdentities()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return v.deepVerifyFailure(result, opts, err.Error(), err)
|
return v.deepVerifyFailure(result, opts, err.Error(), err)
|
||||||
|
|||||||
Reference in new issue
Block a user