mirror of
https://github.com/cupcakearmy/autorestic.git
synced 2025-09-06 18:40:40 +00:00
Compare commits
15 Commits
v1.7.1
...
codespace-
Author | SHA1 | Date | |
---|---|---|---|
|
4e3ae6403b | ||
4d9a2b828e | |||
|
b830667264 | ||
|
83eeb847ac | ||
|
a89ba5a40a | ||
|
6990bf6adc | ||
|
2f407cf211 | ||
489f3078fe | |||
|
e07dd0d991 | ||
|
2b9dc9f17c | ||
|
465bc037c2 | ||
|
37a043afff | ||
|
e91b632181 | ||
|
2b30998b9a | ||
|
49b37a0a9a |
@@ -19,7 +19,7 @@ var DIR, _ = filepath.Abs("./dist")
|
|||||||
var targets = map[string][]string{
|
var targets = map[string][]string{
|
||||||
"darwin": {"amd64", "arm64"},
|
"darwin": {"amd64", "arm64"},
|
||||||
"freebsd": {"386", "amd64", "arm"},
|
"freebsd": {"386", "amd64", "arm"},
|
||||||
"linux": {"386", "amd64", "arm", "arm64"},
|
"linux": {"386", "amd64", "arm", "arm64", "ppc64le", "mips", "mipsle", "mips64", "mips64le"},
|
||||||
"netbsd": {"386", "amd64"},
|
"netbsd": {"386", "amd64"},
|
||||||
"openbsd": {"386", "amd64"},
|
"openbsd": {"386", "amd64"},
|
||||||
}
|
}
|
||||||
|
17
cmd/exec.go
17
cmd/exec.go
@@ -1,6 +1,8 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/cupcakearmy/autorestic/internal"
|
"github.com/cupcakearmy/autorestic/internal"
|
||||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||||
"github.com/cupcakearmy/autorestic/internal/lock"
|
"github.com/cupcakearmy/autorestic/internal/lock"
|
||||||
@@ -18,10 +20,23 @@ var execCmd = &cobra.Command{
|
|||||||
|
|
||||||
selected, err := internal.GetAllOrSelected(cmd, true)
|
selected, err := internal.GetAllOrSelected(cmd, true)
|
||||||
CheckErr(err)
|
CheckErr(err)
|
||||||
|
|
||||||
|
var errors []error
|
||||||
for _, name := range selected {
|
for _, name := range selected {
|
||||||
colors.PrimaryPrint(" Executing on \"%s\" ", name)
|
colors.PrimaryPrint(" Executing on \"%s\" ", name)
|
||||||
backend, _ := internal.GetBackend(name)
|
backend, _ := internal.GetBackend(name)
|
||||||
backend.Exec(args)
|
err := backend.Exec(args)
|
||||||
|
if err != nil {
|
||||||
|
errors = append(errors, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errors) > 0 {
|
||||||
|
for _, err := range errors {
|
||||||
|
colors.Error.Printf("%s\n\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckErr(fmt.Errorf("%d errors were found", len(errors)))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@@ -41,6 +41,7 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().BoolVar(&flags.CI, "ci", false, "CI mode disabled interactive mode and colors and enables verbosity")
|
rootCmd.PersistentFlags().BoolVar(&flags.CI, "ci", false, "CI mode disabled interactive mode and colors and enables verbosity")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&flags.VERBOSE, "verbose", "v", false, "verbose mode")
|
rootCmd.PersistentFlags().BoolVarP(&flags.VERBOSE, "verbose", "v", false, "verbose mode")
|
||||||
rootCmd.PersistentFlags().StringVar(&flags.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary")
|
rootCmd.PersistentFlags().StringVar(&flags.RESTIC_BIN, "restic-bin", "restic", "specify custom restic binary")
|
||||||
|
rootCmd.PersistentFlags().StringVar(&flags.DOCKER_IMAGE, "docker-image", "cupcakearmy/autorestic:"+internal.VERSION, "specify a custom docker image")
|
||||||
cobra.OnInitialize(initConfig)
|
cobra.OnInitialize(initConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
A list of community driven projects. (No official affiliation)
|
A list of community driven projects. (No official affiliation)
|
||||||
|
|
||||||
- SystemD Units: https://gitlab.com/py_crash/autorestic-systemd-units
|
- SystemD Units: <https://gitlab.com/py_crash/autorestic-systemd-units>
|
||||||
- Docker image: https://github.com/pascaliske/docker-autorestic
|
- Docker image: <https://github.com/pascaliske/docker-autorestic>
|
||||||
- Ansible Role: https://github.com/adsanz/ansible-restic-role
|
- Ansible Role: <https://github.com/adsanz/ansible-restic-role>
|
||||||
- Ansible Role: https://github.com/ItsNotGoodName/ansible-role-autorestic
|
- Ansible Role: <https://github.com/ItsNotGoodName/ansible-role-autorestic>
|
||||||
- Ansible Role: https://github.com/FuzzyMistborn/ansible-role-autorestic
|
- Ansible Role: <https://github.com/FuzzyMistborn/ansible-role-autorestic>
|
||||||
|
- Ansible Role: <https://0xacab.org/varac-projects/ansible-role-autorestic>
|
||||||
|
|
||||||
> :ToCPrevNext
|
> :ToCPrevNext
|
||||||
|
@@ -21,7 +21,7 @@ locations:
|
|||||||
keep-weekly: 1 # keep 1 last weekly snapshots
|
keep-weekly: 1 # keep 1 last weekly snapshots
|
||||||
keep-monthly: 12 # keep 12 last monthly snapshots
|
keep-monthly: 12 # keep 12 last monthly snapshots
|
||||||
keep-yearly: 7 # keep 7 last yearly snapshots
|
keep-yearly: 7 # keep 7 last yearly snapshots
|
||||||
keep-within: '2w' # keep snapshots from the last 2 weeks
|
keep-within: '14d' # keep snapshots from the last 14 days
|
||||||
```
|
```
|
||||||
|
|
||||||
## Globally
|
## Globally
|
||||||
|
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/cupcakearmy/autorestic/internal/colors"
|
"github.com/cupcakearmy/autorestic/internal/colors"
|
||||||
|
"github.com/cupcakearmy/autorestic/internal/flags"
|
||||||
)
|
)
|
||||||
|
|
||||||
type BackendRest struct {
|
type BackendRest struct {
|
||||||
@@ -121,13 +122,17 @@ func (b Backend) validate() error {
|
|||||||
}
|
}
|
||||||
options := ExecuteOptions{Envs: env, Silent: true}
|
options := ExecuteOptions{Envs: env, Silent: true}
|
||||||
// Check if already initialized
|
// Check if already initialized
|
||||||
_, _, err = ExecuteResticCommand(options, "check")
|
cmd := []string{"check"}
|
||||||
|
cmd = append(cmd, combineBackendOptions("check", b)...)
|
||||||
|
_, _, err = ExecuteResticCommand(options, cmd...)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
} else {
|
} else {
|
||||||
// If not initialize
|
// If not initialize
|
||||||
colors.Body.Printf("Initializing backend \"%s\"...\n", b.name)
|
colors.Body.Printf("Initializing backend \"%s\"...\n", b.name)
|
||||||
_, _, err := ExecuteResticCommand(options, "init")
|
cmd := []string{"init"}
|
||||||
|
cmd = append(cmd, combineBackendOptions("init", b)...)
|
||||||
|
_, _, err := ExecuteResticCommand(options, cmd...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -160,7 +165,6 @@ func (b Backend) ExecDocker(l Location, args []string) (int, string, error) {
|
|||||||
args = append([]string{"restic"}, args...)
|
args = append([]string{"restic"}, args...)
|
||||||
docker := []string{
|
docker := []string{
|
||||||
"run", "--rm",
|
"run", "--rm",
|
||||||
"--pull", "always",
|
|
||||||
"--entrypoint", "ash",
|
"--entrypoint", "ash",
|
||||||
"--workdir", dir,
|
"--workdir", dir,
|
||||||
"--volume", volume + ":" + dir,
|
"--volume", volume + ":" + dir,
|
||||||
@@ -194,6 +198,7 @@ func (b Backend) ExecDocker(l Location, args []string) (int, string, error) {
|
|||||||
for key, value := range env {
|
for key, value := range env {
|
||||||
docker = append(docker, "--env", key+"="+value)
|
docker = append(docker, "--env", key+"="+value)
|
||||||
}
|
}
|
||||||
docker = append(docker, "cupcakearmy/autorestic:"+VERSION, "-c", strings.Join(args, " "))
|
|
||||||
|
docker = append(docker, flags.DOCKER_IMAGE, "-c", strings.Join(args, " "))
|
||||||
return ExecuteCommand(options, docker...)
|
return ExecuteCommand(options, docker...)
|
||||||
}
|
}
|
||||||
|
225
internal/backend_test.go
Normal file
225
internal/backend_test.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGenerateRepo(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("empty backend", func(t *testing.T) {
|
||||||
|
b := Backend{
|
||||||
|
name: "empty backend",
|
||||||
|
Type: "",
|
||||||
|
}
|
||||||
|
_, err := b.generateRepo()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Error expected for empty backend type")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("local backend", func(t *testing.T) {
|
||||||
|
b := Backend{
|
||||||
|
name: "local backend",
|
||||||
|
Type: "local",
|
||||||
|
Path: "/foo/bar",
|
||||||
|
}
|
||||||
|
result, err := b.generateRepo()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result, "/foo/bar")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("local backend with homedir prefix", func(t *testing.T) {
|
||||||
|
b := Backend{
|
||||||
|
name: "local backend",
|
||||||
|
Type: "local",
|
||||||
|
Path: "~/foo/bar",
|
||||||
|
}
|
||||||
|
result, err := b.generateRepo()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result, fmt.Sprintf("%s/foo/bar", os.Getenv("HOME")))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("local backend with config file", func(t *testing.T) {
|
||||||
|
// config file path should always be present from initConfig
|
||||||
|
viper.SetConfigFile("/tmp/.autorestic.yml")
|
||||||
|
defer viper.Reset()
|
||||||
|
|
||||||
|
b := Backend{
|
||||||
|
name: "local backend",
|
||||||
|
Type: "local",
|
||||||
|
}
|
||||||
|
result, err := b.generateRepo()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result, "/tmp")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rest backend with valid path", func(t *testing.T) {
|
||||||
|
b := Backend{
|
||||||
|
name: "rest backend",
|
||||||
|
Type: "rest",
|
||||||
|
Path: "http://localhost:8000/foo",
|
||||||
|
}
|
||||||
|
result, err := b.generateRepo()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result, "rest:http://localhost:8000/foo")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rest backend with user", func(t *testing.T) {
|
||||||
|
b := Backend{
|
||||||
|
name: "rest backend",
|
||||||
|
Type: "rest",
|
||||||
|
Path: "http://localhost:8000/foo",
|
||||||
|
Rest: BackendRest{
|
||||||
|
User: "user",
|
||||||
|
Password: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := b.generateRepo()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result, "rest:http://user@localhost:8000/foo")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("rest backend with user and password", func(t *testing.T) {
|
||||||
|
b := Backend{
|
||||||
|
name: "rest backend",
|
||||||
|
Type: "rest",
|
||||||
|
Path: "http://localhost:8000/foo",
|
||||||
|
Rest: BackendRest{
|
||||||
|
User: "user",
|
||||||
|
Password: "pass",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := b.generateRepo()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result, "rest:http://user:pass@localhost:8000/foo")
|
||||||
|
})
|
||||||
|
|
||||||
|
backendTests := []struct {
|
||||||
|
name string
|
||||||
|
backend Backend
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{name: "b2 backend", backend: Backend{name: "b2", Type: "b2", Path: "foo"}, want: "b2:foo"},
|
||||||
|
{name: "azure backend", backend: Backend{name: "azure", Type: "azure", Path: "foo"}, want: "azure:foo"},
|
||||||
|
{name: "gs backend", backend: Backend{name: "gs", Type: "gs", Path: "foo"}, want: "gs:foo"},
|
||||||
|
{name: "s3 backend", backend: Backend{name: "s3", Type: "s3", Path: "foo"}, want: "s3:foo"},
|
||||||
|
{name: "sftp backend", backend: Backend{name: "sftp", Type: "sftp", Path: "foo"}, want: "sftp:foo"},
|
||||||
|
{name: "rclone backend", backend: Backend{name: "rclone", Type: "rclone", Path: "foo"}, want: "rclone:foo"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range backendTests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, err := tt.backend.generateRepo()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, got, tt.want)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetEnv(t *testing.T) {
|
||||||
|
t.Run("env in key field", func(t *testing.T) {
|
||||||
|
b := Backend{
|
||||||
|
name: "",
|
||||||
|
Type: "local",
|
||||||
|
Path: "/foo/bar",
|
||||||
|
Key: "secret123",
|
||||||
|
}
|
||||||
|
result, err := b.getEnv()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
|
||||||
|
assertEqual(t, result["RESTIC_PASSWORD"], "secret123")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("env in config file", func(t *testing.T) {
|
||||||
|
b := Backend{
|
||||||
|
name: "",
|
||||||
|
Type: "local",
|
||||||
|
Path: "/foo/bar",
|
||||||
|
Env: map[string]string{
|
||||||
|
"B2_ACCOUNT_ID": "foo123",
|
||||||
|
"B2_ACCOUNT_KEY": "foo456",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result, err := b.getEnv()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
|
||||||
|
assertEqual(t, result["RESTIC_PASSWORD"], "")
|
||||||
|
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
|
||||||
|
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("env in Envfile or env vars", func(t *testing.T) {
|
||||||
|
// generate env variables
|
||||||
|
// TODO better way to teardown
|
||||||
|
defer os.Unsetenv("AUTORESTIC_FOO_RESTIC_PASSWORD")
|
||||||
|
defer os.Unsetenv("AUTORESTIC_FOO_B2_ACCOUNT_ID")
|
||||||
|
defer os.Unsetenv("AUTORESTIC_FOO_B2_ACCOUNT_KEY")
|
||||||
|
os.Setenv("AUTORESTIC_FOO_RESTIC_PASSWORD", "secret123")
|
||||||
|
os.Setenv("AUTORESTIC_FOO_B2_ACCOUNT_ID", "foo123")
|
||||||
|
os.Setenv("AUTORESTIC_FOO_B2_ACCOUNT_KEY", "foo456")
|
||||||
|
|
||||||
|
b := Backend{
|
||||||
|
name: "foo",
|
||||||
|
Type: "local",
|
||||||
|
Path: "/foo/bar",
|
||||||
|
}
|
||||||
|
result, err := b.getEnv()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result["RESTIC_REPOSITORY"], "/foo/bar")
|
||||||
|
assertEqual(t, result["RESTIC_PASSWORD"], "secret123")
|
||||||
|
assertEqual(t, result["B2_ACCOUNT_ID"], "foo123")
|
||||||
|
assertEqual(t, result["B2_ACCOUNT_KEY"], "foo456")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate(t *testing.T) {
|
||||||
|
t.Run("no type given", func(t *testing.T) {
|
||||||
|
b := Backend{
|
||||||
|
name: "foo",
|
||||||
|
Type: "",
|
||||||
|
Path: "/foo/bar",
|
||||||
|
}
|
||||||
|
err := b.validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected to get error")
|
||||||
|
}
|
||||||
|
assertEqual(t, err.Error(), "Backend \"foo\" has no \"type\"")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no path given", func(t *testing.T) {
|
||||||
|
b := Backend{
|
||||||
|
name: "foo",
|
||||||
|
Type: "local",
|
||||||
|
Path: "",
|
||||||
|
}
|
||||||
|
err := b.validate()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected to get error")
|
||||||
|
}
|
||||||
|
assertEqual(t, err.Error(), "Backend \"foo\" has no \"path\"")
|
||||||
|
})
|
||||||
|
}
|
@@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VERSION = "1.7.1"
|
const VERSION = "1.7.3"
|
||||||
|
|
||||||
type OptionMap map[string][]interface{}
|
type OptionMap map[string][]interface{}
|
||||||
type Options map[string]OptionMap
|
type Options map[string]OptionMap
|
||||||
@@ -303,7 +303,17 @@ func getOptions(options Options, keys []string) []string {
|
|||||||
return selected
|
return selected
|
||||||
}
|
}
|
||||||
|
|
||||||
func combineOptions(key string, l Location, b Backend) []string {
|
func combineBackendOptions(key string, b Backend) []string {
|
||||||
|
// Priority: backend > global
|
||||||
|
var options []string
|
||||||
|
gFlags := getOptions(GetConfig().Global, []string{key})
|
||||||
|
bFlags := getOptions(b.Options, []string{"all", key})
|
||||||
|
options = append(options, gFlags...)
|
||||||
|
options = append(options, bFlags...)
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func combineAllOptions(key string, l Location, b Backend) []string {
|
||||||
// Priority: location > backend > global
|
// Priority: location > backend > global
|
||||||
var options []string
|
var options []string
|
||||||
gFlags := getOptions(GetConfig().Global, []string{key})
|
gFlags := getOptions(GetConfig().Global, []string{key})
|
||||||
|
164
internal/config_test.go
Normal file
164
internal/config_test.go
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOptionToString(t *testing.T) {
|
||||||
|
t.Run("no prefix", func(t *testing.T) {
|
||||||
|
opt := "test"
|
||||||
|
result := optionToString(opt)
|
||||||
|
assertEqual(t, result, "--test")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("single prefix", func(t *testing.T) {
|
||||||
|
opt := "-test"
|
||||||
|
result := optionToString(opt)
|
||||||
|
assertEqual(t, result, "-test")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("double prefix", func(t *testing.T) {
|
||||||
|
opt := "--test"
|
||||||
|
result := optionToString(opt)
|
||||||
|
assertEqual(t, result, "--test")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendOneOptionToSlice(t *testing.T) {
|
||||||
|
t.Run("string flag", func(t *testing.T) {
|
||||||
|
result := []string{}
|
||||||
|
optionMap := OptionMap{"string-flag": []interface{}{"/root"}}
|
||||||
|
|
||||||
|
appendOptionsToSlice(&result, optionMap)
|
||||||
|
expected := []string{
|
||||||
|
"--string-flag", "/root",
|
||||||
|
}
|
||||||
|
assertSliceEqual(t, result, expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bool flag", func(t *testing.T) {
|
||||||
|
result := []string{}
|
||||||
|
optionMap := OptionMap{"boolean-flag": []interface{}{true}}
|
||||||
|
|
||||||
|
appendOptionsToSlice(&result, optionMap)
|
||||||
|
expected := []string{
|
||||||
|
"--boolean-flag",
|
||||||
|
}
|
||||||
|
assertSliceEqual(t, result, expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("int flag", func(t *testing.T) {
|
||||||
|
result := []string{}
|
||||||
|
optionMap := OptionMap{"int-flag": []interface{}{123}}
|
||||||
|
|
||||||
|
appendOptionsToSlice(&result, optionMap)
|
||||||
|
expected := []string{
|
||||||
|
"--int-flag", "123",
|
||||||
|
}
|
||||||
|
assertSliceEqual(t, result, expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendMultipleOptionsToSlice(t *testing.T) {
|
||||||
|
result := []string{}
|
||||||
|
optionMap := OptionMap{
|
||||||
|
"string-flag": []interface{}{"/root"},
|
||||||
|
"int-flag": []interface{}{123},
|
||||||
|
}
|
||||||
|
|
||||||
|
appendOptionsToSlice(&result, optionMap)
|
||||||
|
expected := []string{
|
||||||
|
"--string-flag", "/root",
|
||||||
|
"--int-flag", "123",
|
||||||
|
}
|
||||||
|
if len(result) != len(expected) {
|
||||||
|
t.Errorf("got length %d, want length %d", len(result), len(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
// checks that expected option comes after flag, regardless of key order in map
|
||||||
|
for i, v := range expected {
|
||||||
|
v = strings.TrimPrefix(v, "--")
|
||||||
|
|
||||||
|
if value, ok := optionMap[v]; ok {
|
||||||
|
if val, ok := value[0].(int); ok {
|
||||||
|
if expected[i+1] != strconv.Itoa(val) {
|
||||||
|
t.Errorf("Flags and options order are mismatched. got %v, want %v", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppendOptionWithMultipleValuesToSlice(t *testing.T) {
|
||||||
|
result := []string{}
|
||||||
|
optionMap := OptionMap{
|
||||||
|
"string-flag": []interface{}{"/root", "/bin"},
|
||||||
|
}
|
||||||
|
|
||||||
|
appendOptionsToSlice(&result, optionMap)
|
||||||
|
expected := []string{
|
||||||
|
"--string-flag", "/root",
|
||||||
|
"--string-flag", "/bin",
|
||||||
|
}
|
||||||
|
assertSliceEqual(t, result, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOptionsOneKey(t *testing.T) {
|
||||||
|
optionMap := OptionMap{
|
||||||
|
"string-flag": []interface{}{"/root"},
|
||||||
|
}
|
||||||
|
options := Options{"backend": optionMap}
|
||||||
|
keys := []string{"backend"}
|
||||||
|
|
||||||
|
result := getOptions(options, keys)
|
||||||
|
expected := []string{
|
||||||
|
"--string-flag", "/root",
|
||||||
|
}
|
||||||
|
assertSliceEqual(t, result, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetOptionsMultipleKeys(t *testing.T) {
|
||||||
|
firstOptionMap := OptionMap{
|
||||||
|
"string-flag": []interface{}{"/root"},
|
||||||
|
}
|
||||||
|
secondOptionMap := OptionMap{
|
||||||
|
"boolean-flag": []interface{}{true},
|
||||||
|
"int-flag": []interface{}{123},
|
||||||
|
}
|
||||||
|
options := Options{
|
||||||
|
"all": firstOptionMap,
|
||||||
|
"forget": secondOptionMap,
|
||||||
|
}
|
||||||
|
keys := []string{"all", "forget"}
|
||||||
|
|
||||||
|
result := getOptions(options, keys)
|
||||||
|
expected := []string{
|
||||||
|
"--string-flag", "/root",
|
||||||
|
"--boolean-flag",
|
||||||
|
"--int-flag", "123",
|
||||||
|
}
|
||||||
|
reflect.DeepEqual(result, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertEqual[T comparable](t testing.TB, result, expected T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("got %v, want %v", result, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertSliceEqual(t testing.TB, result, expected []string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if len(result) != len(expected) {
|
||||||
|
t.Errorf("got length %d, want length %d", len(result), len(expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range result {
|
||||||
|
assertEqual(t, result[i], expected[i])
|
||||||
|
}
|
||||||
|
}
|
@@ -5,4 +5,5 @@ var (
|
|||||||
VERBOSE bool = false
|
VERBOSE bool = false
|
||||||
CRON_LEAN bool = false
|
CRON_LEAN bool = false
|
||||||
RESTIC_BIN string
|
RESTIC_BIN string
|
||||||
|
DOCKER_IMAGE string
|
||||||
)
|
)
|
||||||
|
@@ -74,12 +74,8 @@ func (l Location) validate() error {
|
|||||||
if from, err := GetPathRelativeToConfig(path); err != nil {
|
if from, err := GetPathRelativeToConfig(path); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
if stat, err := os.Stat(from); err != nil {
|
if _, err := os.Stat(from); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
|
||||||
if !stat.IsDir() {
|
|
||||||
return fmt.Errorf("\"%s\" is not valid directory for location \"%s\"", from, l.name)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,7 +216,7 @@ func (l Location) Backup(cron bool, specificBackend string) []error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd := []string{"backup"}
|
cmd := []string{"backup"}
|
||||||
cmd = append(cmd, combineOptions("backup", l, backend)...)
|
cmd = append(cmd, combineAllOptions("backup", l, backend)...)
|
||||||
if cron {
|
if cron {
|
||||||
cmd = append(cmd, "--tag", buildTag("cron"))
|
cmd = append(cmd, "--tag", buildTag("cron"))
|
||||||
}
|
}
|
||||||
@@ -324,7 +320,12 @@ after:
|
|||||||
func (l Location) Forget(prune bool, dry bool) error {
|
func (l Location) Forget(prune bool, dry bool) error {
|
||||||
colors.PrimaryPrint("Forgetting for location \"%s\"", l.name)
|
colors.PrimaryPrint("Forgetting for location \"%s\"", l.name)
|
||||||
|
|
||||||
for _, to := range l.To {
|
backendsToForget := l.To
|
||||||
|
for _, copyBackends := range l.CopyOption {
|
||||||
|
backendsToForget = append(backendsToForget, copyBackends...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, to := range backendsToForget {
|
||||||
backend, _ := GetBackend(to)
|
backend, _ := GetBackend(to)
|
||||||
colors.Secondary.Printf("For backend \"%s\"\n", backend.name)
|
colors.Secondary.Printf("For backend \"%s\"\n", backend.name)
|
||||||
env, err := backend.getEnv()
|
env, err := backend.getEnv()
|
||||||
@@ -341,7 +342,7 @@ func (l Location) Forget(prune bool, dry bool) error {
|
|||||||
if dry {
|
if dry {
|
||||||
cmd = append(cmd, "--dry-run")
|
cmd = append(cmd, "--dry-run")
|
||||||
}
|
}
|
||||||
cmd = append(cmd, combineOptions("forget", l, backend)...)
|
cmd = append(cmd, combineAllOptions("forget", l, backend)...)
|
||||||
_, _, err = ExecuteResticCommand(options, cmd...)
|
_, _, err = ExecuteResticCommand(options, cmd...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
93
internal/location_test.go
Normal file
93
internal/location_test.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package internal
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestGetType(t *testing.T) {
|
||||||
|
|
||||||
|
t.Run("TypeLocal", func(t *testing.T) {
|
||||||
|
l := Location{
|
||||||
|
Type: "local",
|
||||||
|
}
|
||||||
|
result, err := l.getType()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result, TypeLocal)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TypeVolume", func(t *testing.T) {
|
||||||
|
l := Location{
|
||||||
|
Type: "volume",
|
||||||
|
}
|
||||||
|
result, err := l.getType()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result, TypeVolume)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Empty type", func(t *testing.T) {
|
||||||
|
l := Location{
|
||||||
|
Type: "",
|
||||||
|
}
|
||||||
|
result, err := l.getType()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
assertEqual(t, result, TypeLocal)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Invalid type", func(t *testing.T) {
|
||||||
|
l := Location{
|
||||||
|
Type: "foo",
|
||||||
|
}
|
||||||
|
_, err := l.getType()
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildTag(t *testing.T) {
|
||||||
|
result := buildTag("foo", "bar")
|
||||||
|
expected := "ar:foo:bar"
|
||||||
|
assertEqual(t, result, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetLocationTags(t *testing.T) {
|
||||||
|
l := Location{
|
||||||
|
name: "foo",
|
||||||
|
}
|
||||||
|
result := l.getLocationTags()
|
||||||
|
expected := "ar:location:foo"
|
||||||
|
assertEqual(t, result, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasBackend(t *testing.T) {
|
||||||
|
t.Run("backend present", func(t *testing.T) {
|
||||||
|
l := Location{
|
||||||
|
name: "foo",
|
||||||
|
To: []string{"foo", "bar"},
|
||||||
|
}
|
||||||
|
result := l.hasBackend("foo")
|
||||||
|
assertEqual(t, result, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("backend absent", func(t *testing.T) {
|
||||||
|
l := Location{
|
||||||
|
name: "foo",
|
||||||
|
To: []string{"bar", "baz"},
|
||||||
|
}
|
||||||
|
result := l.hasBackend("foo")
|
||||||
|
assertEqual(t, result, false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildRestoreCommand(t *testing.T) {
|
||||||
|
l := Location{
|
||||||
|
name: "foo",
|
||||||
|
}
|
||||||
|
result := buildRestoreCommand(l, "to", "snapshot", []string{"options"})
|
||||||
|
expected := []string{"restore", "--target", "to", "--tag", "ar:location:foo", "snapshot", "options"}
|
||||||
|
assertSliceEqual(t, result, expected)
|
||||||
|
}
|
@@ -14,6 +14,10 @@ var lock *viper.Viper
|
|||||||
var file string
|
var file string
|
||||||
var once sync.Once
|
var once sync.Once
|
||||||
|
|
||||||
|
const (
|
||||||
|
RUNNING = "running"
|
||||||
|
)
|
||||||
|
|
||||||
func getLock() *viper.Viper {
|
func getLock() *viper.Viper {
|
||||||
if lock == nil {
|
if lock == nil {
|
||||||
|
|
||||||
@@ -37,36 +41,38 @@ func getLock() *viper.Viper {
|
|||||||
return lock
|
return lock
|
||||||
}
|
}
|
||||||
|
|
||||||
func setLock(locked bool) error {
|
func setLockValue(key string, value interface{}) (*viper.Viper, error) {
|
||||||
lock := getLock()
|
lock := getLock()
|
||||||
if locked {
|
|
||||||
running := lock.GetBool("running")
|
if key == RUNNING {
|
||||||
if running {
|
value := value.(bool)
|
||||||
|
if value && lock.GetBool(key) {
|
||||||
colors.Error.Println("an instance is already running. exiting")
|
colors.Error.Println("an instance is already running. exiting")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lock.Set("running", locked)
|
|
||||||
|
lock.Set(key, value)
|
||||||
if err := lock.WriteConfigAs(file); err != nil {
|
if err := lock.WriteConfigAs(file); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
return nil
|
return lock, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCron(location string) int64 {
|
func GetCron(location string) int64 {
|
||||||
lock := getLock()
|
return getLock().GetInt64("cron." + location)
|
||||||
return lock.GetInt64("cron." + location)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetCron(location string, value int64) {
|
func SetCron(location string, value int64) {
|
||||||
lock.Set("cron."+location, value)
|
setLockValue("cron."+location, value)
|
||||||
lock.WriteConfigAs(file)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Lock() error {
|
func Lock() error {
|
||||||
return setLock(true)
|
_, err := setLockValue(RUNNING, true)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Unlock() error {
|
func Unlock() error {
|
||||||
return setLock(false)
|
_, err := setLockValue(RUNNING, false)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
114
internal/lock/lock_test.go
Normal file
114
internal/lock/lock_test.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package lock
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testDirectory = "autorestic_test_tmp"
|
||||||
|
|
||||||
|
// All tests must share the same lock file as it is only initialized once
|
||||||
|
func setup(t *testing.T) {
|
||||||
|
d, err := os.MkdirTemp("", testDirectory)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("error creating temp dir: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// set config file location
|
||||||
|
viper.SetConfigFile(d + "/.autorestic.yml")
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.RemoveAll(d)
|
||||||
|
viper.Reset()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLock(t *testing.T) {
|
||||||
|
setup(t)
|
||||||
|
|
||||||
|
t.Run("getLock", func(t *testing.T) {
|
||||||
|
result := getLock().GetBool(RUNNING)
|
||||||
|
|
||||||
|
if result {
|
||||||
|
t.Errorf("got %v, want %v", result, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("lock", func(t *testing.T) {
|
||||||
|
lock, err := setLockValue(RUNNING, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := lock.GetBool(RUNNING)
|
||||||
|
if !result {
|
||||||
|
t.Errorf("got %v, want %v", result, true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unlock", func(t *testing.T) {
|
||||||
|
lock, err := setLockValue(RUNNING, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := lock.GetBool(RUNNING)
|
||||||
|
if result {
|
||||||
|
t.Errorf("got %v, want %v", result, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// locking a locked instance exits the instance
|
||||||
|
// this trick to capture os.Exit(1) is discussed here:
|
||||||
|
// https://talks.golang.org/2014/testing.slide#23
|
||||||
|
t.Run("lock twice", func(t *testing.T) {
|
||||||
|
if os.Getenv("CRASH") == "1" {
|
||||||
|
err := Lock()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
// should fail
|
||||||
|
Lock()
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(os.Args[0], "-test.run=TestLock/lock_twice")
|
||||||
|
cmd.Env = append(os.Environ(), "CRASH=1")
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
err, ok := err.(*exec.ExitError)
|
||||||
|
if !ok {
|
||||||
|
t.Error("unexpected error")
|
||||||
|
}
|
||||||
|
expected := "exit status 1"
|
||||||
|
if err.Error() != expected {
|
||||||
|
t.Errorf("got %q, want %q", err.Error(), expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("set cron", func(t *testing.T) {
|
||||||
|
expected := int64(5)
|
||||||
|
SetCron("foo", expected)
|
||||||
|
|
||||||
|
result, err := strconv.ParseInt(getLock().GetString("cron.foo"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("got %d, want %d", result, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("get cron", func(t *testing.T) {
|
||||||
|
expected := int64(5)
|
||||||
|
result := GetCron("foo")
|
||||||
|
|
||||||
|
if result != expected {
|
||||||
|
t.Errorf("got %d, want %d", result, expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Reference in New Issue
Block a user