Author SHA1 Message Date
sneak 0d6df8c2a2 Label the commit date "commit date" in version and banner (closes #409)
check / check (push) Failing after 23m58s
The value is the commit's date, which the Makefile and the Dockerfile
stamp in, not the time of the build. `vaultik version` labelled it
"build date" and the startup banner "built on". The other version
labels gain one space so every value still starts in one column.

The tests give the commit and the commit date different values, since
the test binary is not stamped and holds "unknown" in both.

Model: opus-5-5
2026-11-10 20:45:02 +00:00
clawbot 13a35a986a Correct two false doc comments (closes #403)
check / check (push) Successful in 13m0s
Two doc comments described behaviour that no longer exists. The comment on `ExportSnapshotMetadata` described an SQL dump uploaded under `snapshots/`; it now lists the steps the function performs and the two keys it writes. The comment on `EnsureStorageBinding` named `vaultik database purge`; the command is `vaultik database delete`. Comments only.

Model: opus-5-5
2026-11-09 07:43:24 +01:00
10 changed files with 94 additions and 33 deletions

No files matched your search

+1 -1
View File
@@ -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, built on %s) starting up at %s.",
w.Bannerf("%s %s by %s (commit %s, commit date %s) starting up at %s.",
globals.Appname, globals.Version, globals.Author,
shortCommit, globals.CommitDate,
startTime.Format(time.RFC3339))
+30
View File
@@ -13,6 +13,7 @@ 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
@@ -112,6 +113,35 @@ 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.
+1 -1
View File
@@ -38,7 +38,7 @@ func writeVersion(out *ui.Writer) {
_, _ = fmt.Fprintf(w, "vaultik %s\n", globals.Version)
_, _ = fmt.Fprintf(w, " commit: %s\n", globals.Commit)
_, _ = fmt.Fprintf(w, " build date: %s\n", globals.CommitDate)
_, _ = 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)
+37
View File
@@ -2,6 +2,7 @@ package cli_test
import (
"bytes"
"runtime"
"strings"
"testing"
@@ -49,6 +50,42 @@ 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
+7 -5
View File
@@ -271,16 +271,18 @@ func (sm *SnapshotManager) CompleteSnapshot(
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:
// 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. 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]
// 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
//
// The only caller (finalizeSnapshotMetadata) does not close the main database
// before calling this method: the index is copied at dbPath while it is still
+4 -3
View File
@@ -31,9 +31,10 @@ var (
errPathsNotInSnapshot = errors.New("requested path(s) not in the snapshot")
errFilesFailedVerify = errors.New("files failed verification")
errDecryptionKeyRequired = errors.New(
"age secret key is missing; set it in the " +
"VAULTIK_AGE_SECRET_KEY environment variable or the " +
"age_secret_key config file field")
"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)\"")
// 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.
-20
View File
@@ -106,23 +106,3 @@ 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)
}
+1 -1
View File
@@ -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
// 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).
//
// Read-only inspection commands (remote info, snapshot list, etc.)
+5
View File
@@ -162,6 +162,11 @@ 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
+8 -2
View File
@@ -23,6 +23,8 @@ 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")
errChunksOutOfOrder = errors.New("chunks out of order")
errChunkHashMismatch = errors.New("chunk hash mismatch")
errNegativeChunkLength = errors.New("chunk length is negative")
@@ -104,9 +106,13 @@ 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. A missing key fails
// here, before anything is downloaded.
// the identities for the database and every blob.
identities, err := v.restoreIdentities()
if err != nil {
return v.deepVerifyFailure(result, opts, err.Error(), err)