This commit is contained in:
Boris Bera 2024-10-14 18:24:10 +00:00 committed by GitHub
commit 44efd64683
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 214 additions and 10 deletions

View File

@ -4,11 +4,25 @@ on:
push: push:
branches: [master] branches: [master]
env:
RESTIC_VERSION: "0.17.1"
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Install restic@${{ env.RESTIC_VERSION }}
run: |
mkdir -p tools/restic
curl --fail --location --silent --show-error --output tools/restic/restic.bz2 \
"https://github.com/restic/restic/releases/download/v${RESTIC_VERSION}/restic_${RESTIC_VERSION}_linux_amd64.bz2"
bzip2 -d tools/restic/restic.bz2
chmod +x tools/restic/restic
echo "$GITHUB_WORKSPACE/tools/restic" >> "$GITHUB_PATH"
- run: restic version
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: '^1.21' go-version: '^1.21'

73
cmd/backup_test.go Normal file
View File

@ -0,0 +1,73 @@
package cmd
import (
"os"
"path"
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)
func runCmd(t *testing.T, args ...string) error {
t.Helper()
viper.Reset()
rootCmd.SetArgs(args)
err := rootCmd.Execute()
return err
}
func TestBackupCmd(t *testing.T) {
workDir := t.TempDir()
// Prepare content to be backed up
locationDir := path.Join(workDir, "my-location")
err := os.Mkdir(locationDir, 0750)
assert.Nil(t, err)
err = os.WriteFile(path.Join(locationDir, "back-me-up.txt"), []byte("hello world"), 0640)
assert.Nil(t, err)
// Write config file
config, err := yaml.Marshal(map[string]interface{}{
"version": 2,
"locations": map[string]map[string]interface{}{
"my-location": {
"type": "local",
"from": []string{locationDir},
"to": []string{"test"},
},
},
"backends": map[string]map[string]interface{}{
"test": {
"type": "local",
"path": path.Join(workDir, "test-backend"),
"key": "supersecret",
},
},
})
assert.Nil(t, err)
configPath := path.Join(workDir, ".autorestic.yml")
err = os.WriteFile(configPath, config, 0640)
assert.Nil(t, err)
// Init repo (not initialized by default)
err = runCmd(t, "exec", "--ci", "-a", "-c", configPath, "init")
assert.Nil(t, err)
// Do the backup
err = runCmd(t, "backup", "--ci", "-a", "-c", configPath)
assert.Nil(t, err)
// Restore in a separate dir
restoreDir := path.Join(workDir, "restore")
err = runCmd(t, "restore", "--ci", "-c", configPath, "-l", "my-location", "--to", restoreDir)
assert.Nil(t, err)
// Check restored file
restoredContent, err := os.ReadFile(path.Join(restoreDir, locationDir, "back-me-up.txt"))
assert.Nil(t, err)
assert.Equal(t, "hello world", string(restoredContent))
}

View File

@ -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. 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.

2
go.mod
View File

@ -12,6 +12,7 @@ require (
github.com/spf13/cobra v1.4.0 github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.11.0 github.com/spf13/viper v1.11.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
@ -35,5 +36,4 @@ require (
golang.org/x/text v0.3.8 // indirect golang.org/x/text v0.3.8 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

View File

@ -24,6 +24,7 @@ type Backend struct {
Path string `mapstructure:"path,omitempty"` Path string `mapstructure:"path,omitempty"`
Key string `mapstructure:"key,omitempty"` Key string `mapstructure:"key,omitempty"`
RequireKey bool `mapstructure:"requireKey,omitempty"` RequireKey bool `mapstructure:"requireKey,omitempty"`
Init bool `mapstructure:"init,omitempty"`
Env map[string]string `mapstructure:"env,omitempty"` Env map[string]string `mapstructure:"env,omitempty"`
Rest BackendRest `mapstructure:"rest,omitempty"` Rest BackendRest `mapstructure:"rest,omitempty"`
Options Options `mapstructure:"options,omitempty"` Options Options `mapstructure:"options,omitempty"`
@ -130,20 +131,44 @@ func (b Backend) validate() error {
return err return err
} }
options := ExecuteOptions{Envs: env, Silent: true} options := ExecuteOptions{Envs: env, Silent: true}
// Check if already initialized
err = b.EnsureInit()
if err != nil {
return err
}
cmd := []string{"check"} cmd := []string{"check"}
cmd = append(cmd, combineBackendOptions("check", b)...) cmd = append(cmd, combineBackendOptions("check", b)...)
_, _, err = ExecuteResticCommand(options, cmd...) _, _, err = ExecuteResticCommand(options, cmd...)
if err == nil { return err
return nil }
} else {
// If not initialize // EnsureInit initializes the backend if it is not already initialized
colors.Body.Printf("Initializing backend \"%s\"...\n", b.name) func (b Backend) EnsureInit() error {
cmd := []string{"init"} env, err := b.getEnv()
cmd = append(cmd, combineBackendOptions("init", b)...) if err != nil {
_, _, err := ExecuteResticCommand(options, cmd...)
return err 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)
initCmd := []string{"init"}
initCmd = append(initCmd, combineBackendOptions("init", b)...)
_, _, err := ExecuteResticCommand(options, initCmd...)
return err
}
return err
} }
func (b Backend) Exec(args []string) error { func (b Backend) Exec(args []string) error {

View File

@ -3,8 +3,10 @@ package internal
import ( import (
"fmt" "fmt"
"os" "os"
"path"
"testing" "testing"
"github.com/cupcakearmy/autorestic/internal/flags"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/assert" "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") 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)
}

View File

@ -222,6 +222,14 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
continue continue
} }
if backend.Init {
err = backend.EnsureInit()
if err != nil {
errors = append(errors, err)
continue
}
}
cmd := []string{"backup"} cmd := []string{"backup"}
cmd = append(cmd, combineAllOptions("backup", l, backend)...) cmd = append(cmd, combineAllOptions("backup", l, backend)...)
if cron { if cron {