Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6da4315f57 | ||
|
|
13a35a986a |
No files matched your search
@@ -38,7 +38,7 @@ func writeVersion(out *ui.Writer) {
|
|||||||
|
|
||||||
_, _ = 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, " 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, " 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)
|
||||||
|
|||||||
@@ -49,6 +49,25 @@ func TestVersionCommandReportsBuildVersion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestVersionCommandLabelsCommitDate checks that the date line is
|
||||||
|
// labelled as the commit's date, which is what the build stamps into
|
||||||
|
// it, and that its value starts in the same column as the commit's.
|
||||||
|
//
|
||||||
|
//nolint:paralleltest // executes a command that reads the global rootFlags
|
||||||
|
func TestVersionCommandLabelsCommitDate(t *testing.T) {
|
||||||
|
out := runVersionCommand(t)
|
||||||
|
|
||||||
|
wantLine := " commit date: " + globals.CommitDate
|
||||||
|
if !strings.Contains(out, wantLine+"\n") {
|
||||||
|
t.Errorf("output has no line %q:\n%s", wantLine, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantCommit := " commit: " + globals.Commit
|
||||||
|
if !strings.Contains(out, wantCommit+"\n") {
|
||||||
|
t.Errorf("output has no line %q:\n%s", wantCommit, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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,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
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ 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(
|
||||||
"age secret key is missing; set it in the " +
|
"decryption key required for restore\n\n" +
|
||||||
"VAULTIK_AGE_SECRET_KEY environment variable or the " +
|
"Set the VAULTIK_AGE_SECRET_KEY environment variable to your " +
|
||||||
"age_secret_key config file field")
|
"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
|
// 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,23 +106,3 @@ 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
|
||||||
// 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.)
|
||||||
|
|||||||
@@ -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
|
// 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
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ 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")
|
||||||
@@ -104,9 +106,13 @@ 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. A missing key fails
|
// the identities for the database and every blob.
|
||||||
// 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