Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b42c4bb325 |
No files matched your search
+1
-1
@@ -90,7 +90,7 @@ func setupGlobals(
|
||||
// blank line. Used both from the fx hook (for subcommand invocations) and
|
||||
// from the root cobra Run handler (for `vaultik` with no subcommand).
|
||||
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,
|
||||
shortCommit, globals.CommitDate,
|
||||
startTime.Format(time.RFC3339))
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"github.com/adrg/xdg"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/vaultik/internal/globals"
|
||||
)
|
||||
|
||||
// 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,
|
||||
// including the flags that suppressed the banner before --json joined
|
||||
// them, so that adding --json cannot regress them.
|
||||
|
||||
@@ -37,13 +37,13 @@ func writeVersion(out *ui.Writer) {
|
||||
w := out.Out()
|
||||
|
||||
_, _ = fmt.Fprintf(w, "vaultik %s\n", globals.Version)
|
||||
_, _ = fmt.Fprintf(w, " commit: %s\n", globals.Commit)
|
||||
_, _ = fmt.Fprintf(w, " commit date: %s\n", globals.CommitDate)
|
||||
_, _ = fmt.Fprintf(w, " go: %s\n", runtime.Version())
|
||||
_, _ = fmt.Fprintf(w, " os/arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
_, _ = fmt.Fprintf(w, " author: %s\n", globals.Author)
|
||||
_, _ = fmt.Fprintf(w, " homepage: %s\n", globals.Homepage)
|
||||
_, _ = fmt.Fprintf(w, " license: %s\n", globals.License)
|
||||
_, _ = fmt.Fprintf(w, " commit: %s\n", globals.Commit)
|
||||
_, _ = fmt.Fprintf(w, " build date: %s\n", globals.CommitDate)
|
||||
_, _ = fmt.Fprintf(w, " go: %s\n", runtime.Version())
|
||||
_, _ = fmt.Fprintf(w, " os/arch: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||
_, _ = fmt.Fprintf(w, " author: %s\n", globals.Author)
|
||||
_, _ = fmt.Fprintf(w, " homepage: %s\n", globals.Homepage)
|
||||
_, _ = fmt.Fprintf(w, " license: %s\n", globals.License)
|
||||
|
||||
if globals.IsDevVersion(globals.Version) {
|
||||
_, _ = fmt.Fprintln(w)
|
||||
|
||||
@@ -2,7 +2,6 @@ package cli_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"runtime"
|
||||
"strings"
|
||||
"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
|
||||
// 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
|
||||
|
||||
@@ -271,18 +271,16 @@ func (sm *SnapshotManager) CompleteSnapshot(
|
||||
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:
|
||||
// 1. Creates a temporary directory for working files
|
||||
// 2. Copies the main database to preserve its state
|
||||
// 3. Cleans the copy to contain only current snapshot data
|
||||
// 4. Vacuums the copy to remove the deleted data
|
||||
// 5. Compresses the copy with zstd
|
||||
// 6. Encrypts the compressed copy with age (always; recipients are required)
|
||||
// 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
|
||||
// 4. Dumps the cleaned database to SQL
|
||||
// 5. Compresses the SQL dump with zstd
|
||||
// 6. Encrypts the compressed data with age (always; recipients are required)
|
||||
// 7. Uploads to S3 at: snapshots/{snapshot-id}.sql.zst[.age]
|
||||
//
|
||||
// The only caller (finalizeSnapshotMetadata) does not close the main database
|
||||
// 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")
|
||||
errFilesFailedVerify = errors.New("files failed verification")
|
||||
errDecryptionKeyRequired = errors.New(
|
||||
"decryption key required for restore\n\n" +
|
||||
"Set the VAULTIK_AGE_SECRET_KEY environment variable to your " +
|
||||
"age private key file:\n" +
|
||||
" export VAULTIK_AGE_SECRET_KEY=\"$(cat vaultik_backup_private_key.txt)\"")
|
||||
"age secret key is missing; set it in the " +
|
||||
"VAULTIK_AGE_SECRET_KEY environment variable or the " +
|
||||
"age_secret_key config file field")
|
||||
// errInvalidAgeSecretKey is returned when the configured key does not
|
||||
// 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.
|
||||
|
||||
@@ -106,3 +106,23 @@ func TestRestoreIdentitiesMissingKey(t *testing.T) {
|
||||
_, err := v.restoreIdentities()
|
||||
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.
|
||||
// - When the two differ, refuse with an error that tells the user
|
||||
// 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).
|
||||
//
|
||||
// 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
|
||||
//
|
||||
//nolint:ireturn // afero.Fs is the filesystem abstraction by design
|
||||
|
||||
@@ -22,9 +22,7 @@ import (
|
||||
|
||||
// Sentinel errors for snapshot verification failures.
|
||||
var (
|
||||
errVerificationFailed = errors.New("verification failed")
|
||||
errSecretKeyRequired = errors.New(
|
||||
"VAULTIK_AGE_SECRET_KEY not set; required for deep verification")
|
||||
errVerificationFailed = errors.New("verification failed")
|
||||
errChunksOutOfOrder = errors.New("chunks out of order")
|
||||
errChunkHashMismatch = errors.New("chunk hash mismatch")
|
||||
errNegativeChunkLength = errors.New("chunk length is negative")
|
||||
@@ -106,13 +104,9 @@ func (v *Vaultik) RunDeepVerify(snapshotID string, opts *VerifyOptions) error {
|
||||
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
|
||||
// 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()
|
||||
if err != nil {
|
||||
return v.deepVerifyFailure(result, opts, err.Error(), err)
|
||||
|
||||
Reference in new issue
Block a user