From 5ed1af31e0821e0f99001e72fd1ab4b452628a71 Mon Sep 17 00:00:00 2001 From: Boris Bera Date: Mon, 14 Oct 2024 13:56:22 -0400 Subject: [PATCH] feat(backend): add init option to backend config When this option is set to `true`, the backend will automatically get initialized during the backup process. This applies to invoking `autorestic backup` or when `autorestic cron` actualy performs a backup. --- docs/pages/backend/index.md | 16 +++++++++ internal/backend.go | 41 +++++++++++++++++----- internal/backend_test.go | 68 +++++++++++++++++++++++++++++++++++++ internal/location.go | 8 +++++ 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/docs/pages/backend/index.md b/docs/pages/backend/index.md index 01a14fa..545abcf 100644 --- a/docs/pages/backend/index.md +++ b/docs/pages/backend/index.md @@ -38,3 +38,19 @@ backends: ``` With this setting, if a key is missing, `autorestic` will crash instead of generating a new key and updating your config file. + +## Automatic Backend Initialization + +`autorestic` is able to automatically initialize backends for you. This is done by setting `init: true` in the config for a given backend. For example: + +```yaml | .autorestic.yml +backend: + foo: + type: ... + path: ... + init: true +``` + +When you set `init: true` on a backend config, `autorestic` will automatically initialize the underlying `restic` repository that powers the backend if it's not already initialized. In practice, this means that the backend will be initialized the first time it is being backed up to. + +This option is helpful in cases where you want to automate the configuration of `autorestic`. This means that instead of running `autorestic exec init -b ...` manually when you create a new backend, you can let `autorestic` initialize it for you. diff --git a/internal/backend.go b/internal/backend.go index 669935a..fd76990 100644 --- a/internal/backend.go +++ b/internal/backend.go @@ -24,6 +24,7 @@ type Backend struct { Path string `mapstructure:"path,omitempty"` Key string `mapstructure:"key,omitempty"` RequireKey bool `mapstructure:"requireKey,omitempty"` + Init bool `mapstructure:"init,omitempty"` Env map[string]string `mapstructure:"env,omitempty"` Rest BackendRest `mapstructure:"rest,omitempty"` Options Options `mapstructure:"options,omitempty"` @@ -130,20 +131,44 @@ func (b Backend) validate() error { return err } options := ExecuteOptions{Envs: env, Silent: true} - // Check if already initialized + + err = b.EnsureInit() + if err != nil { + return err + } + cmd := []string{"check"} cmd = append(cmd, combineBackendOptions("check", b)...) _, _, err = ExecuteResticCommand(options, cmd...) - if err == nil { - return nil - } else { - // If not initialize + return err +} + +// EnsureInit initializes the backend if it is not already initialized +func (b Backend) EnsureInit() error { + env, err := b.getEnv() + if err != nil { + return err + } + options := ExecuteOptions{Envs: env, Silent: true} + + checkInitCmd := []string{"cat", "config"} + checkInitCmd = append(checkInitCmd, combineBackendOptions("cat", b)...) + _, _, err = ExecuteResticCommand(options, checkInitCmd...) + + // Note that `restic` has a special exit code (10) to indicate that the + // repository does not exist. This exit code was introduced in `restic@0.17.0` + // on 2024-07-26. We're not using it here because this is a too recent and + // people on older versions of `restic` won't have this feature work correctly. + // See: https://restic.readthedocs.io/en/latest/075_scripting.html#exit-codes + if err != nil { colors.Body.Printf("Initializing backend \"%s\"...\n", b.name) - cmd := []string{"init"} - cmd = append(cmd, combineBackendOptions("init", b)...) - _, _, err := ExecuteResticCommand(options, cmd...) + initCmd := []string{"init"} + initCmd = append(initCmd, combineBackendOptions("init", b)...) + _, _, err := ExecuteResticCommand(options, initCmd...) return err } + + return err } func (b Backend) Exec(args []string) error { diff --git a/internal/backend_test.go b/internal/backend_test.go index e65e4fe..508297f 100644 --- a/internal/backend_test.go +++ b/internal/backend_test.go @@ -3,8 +3,10 @@ package internal import ( "fmt" "os" + "path" "testing" + "github.com/cupcakearmy/autorestic/internal/flags" "github.com/spf13/viper" "github.com/stretchr/testify/assert" ) @@ -263,3 +265,69 @@ func TestValidate(t *testing.T) { assert.EqualError(t, err, "backend foo requires a key but none was provided") }) } + +func TestValidateInitsRepo(t *testing.T) { + // This is normally initialized by the cobra commands but they don't run in + // this test so we do it ourselves. + flags.RESTIC_BIN = "restic" + + workDir := t.TempDir() + + b := Backend{ + name: "test", + Type: "local", + Path: path.Join(workDir, "backend"), + Key: "supersecret", + } + + config = &Config{Backends: map[string]Backend{"test": b}} + defer func() { config = nil }() + + // Check should fail because the repo doesn't exist + err := b.Exec([]string{"check"}) + assert.Error(t, err) + + err = b.validate() + assert.NoError(t, err) + + // Check should pass now + err = b.Exec([]string{"check"}) + assert.NoError(t, err) +} + +func TestEnsureInit(t *testing.T) { + // This is normally initialized by the cobra commands but they don't run in + // this test so we do it ourselves. + flags.RESTIC_BIN = "restic" + + workDir := t.TempDir() + + b := Backend{ + name: "test", + Type: "local", + Path: path.Join(workDir, "backend"), + Key: "supersecret", + } + + config = &Config{Backends: map[string]Backend{"test": b}} + defer func() { config = nil }() + + // Check should fail because the repo doesn't exist + err := b.Exec([]string{"check"}) + assert.Error(t, err) + + err = b.EnsureInit() + assert.NoError(t, err) + + // Check should pass now + err = b.Exec([]string{"check"}) + assert.NoError(t, err) + + // Run again to make sure it's idempotent + err = b.EnsureInit() + assert.NoError(t, err) + + // Check should still pass + err = b.Exec([]string{"check"}) + assert.NoError(t, err) +} diff --git a/internal/location.go b/internal/location.go index a20efa7..e8cc84a 100644 --- a/internal/location.go +++ b/internal/location.go @@ -222,6 +222,14 @@ func (l Location) Backup(cron bool, specificBackend string) []error { continue } + if backend.Init { + err = backend.EnsureInit() + if err != nil { + errors = append(errors, err) + continue + } + } + cmd := []string{"backup"} cmd = append(cmd, combineAllOptions("backup", l, backend)...) if cron {