Compare commits

...

16 commits

Author SHA1 Message Date
Alexander Neumann
abb1dc4eb6 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
8d21bb92db Add tests for invalid configs 2018-06-10 12:27:52 +02:00
Alexander Neumann
0b3c402801 Move options package to ui/options 2018-06-10 12:27:52 +02:00
Alexander Neumann
b3b70002ab wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
4916ba7a8a wip name 2018-06-10 12:27:52 +02:00
Alexander Neumann
ea565df3e8 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
0758c92afc wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
8b0092908a wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
ffd7bc1021 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
6bad560324 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
7ad648c686 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
0c078cc205 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
1fbcf63830 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
740e2d6139 wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
aaef54559a wip 2018-06-10 12:27:52 +02:00
Alexander Neumann
722517c480 wip 2018-06-10 12:27:52 +02:00
28 changed files with 845 additions and 41 deletions

View file

@ -21,6 +21,7 @@ import (
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/textfile"
"github.com/restic/restic/internal/ui"
"github.com/restic/restic/internal/ui/config"
"github.com/restic/restic/internal/ui/termstatus"
)
@ -43,6 +44,11 @@ given as the arguments.
},
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
err := config.ApplyFlags(&backupOptions.Config, cmd.Flags())
if err != nil {
return err
}
if backupOptions.Stdin && backupOptions.FilesFrom == "-" {
return errors.Fatal("cannot use both `--stdin` and `--files-from -`")
}
@ -51,7 +57,7 @@ given as the arguments.
term := termstatus.New(globalOptions.stdout, globalOptions.stderr, globalOptions.Quiet)
t.Go(func() error { term.Run(t.Context(globalOptions.ctx)); return nil })
err := runBackup(backupOptions, globalOptions, term, args)
err = runBackup(backupOptions, globalOptions, term, args)
if err != nil {
return err
}
@ -62,9 +68,10 @@ given as the arguments.
// BackupOptions bundles all options for the backup command.
type BackupOptions struct {
Config config.Backup
Parent string
Force bool
Excludes []string
ExcludeFiles []string
ExcludeOtherFS bool
ExcludeIfPresent []string
@ -86,7 +93,9 @@ func init() {
f := cmdBackup.Flags()
f.StringVar(&backupOptions.Parent, "parent", "", "use this parent snapshot (default: last snapshot in the repo that has the same target files/directories)")
f.BoolVarP(&backupOptions.Force, "force", "f", false, `force re-reading the target files/directories (overrides the "parent" flag)`)
f.StringArrayVarP(&backupOptions.Excludes, "exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
f.StringArrayP("exclude", "e", nil, "exclude a `pattern` (can be specified multiple times)")
f.StringArrayVar(&backupOptions.ExcludeFiles, "exclude-file", nil, "read exclude patterns from a `file` (can be specified multiple times)")
f.BoolVarP(&backupOptions.ExcludeOtherFS, "one-file-system", "x", false, "exclude other file systems")
f.StringArrayVar(&backupOptions.ExcludeIfPresent, "exclude-if-present", nil, "takes filename[:header], exclude contents of directories containing filename (except filename itself) if header of that file is as provided (can be specified multiple times)")
@ -188,12 +197,12 @@ func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error {
// collectRejectFuncs returns a list of all functions which may reject data
// from being saved in a snapshot
func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectFunc, err error) {
func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectFunc, excludes []string, err error) {
// allowed devices
if opts.ExcludeOtherFS {
f, err := rejectByDevice(targets)
if err != nil {
return nil, err
return nil, nil, err
}
fs = append(fs, f)
}
@ -202,19 +211,21 @@ func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets
if repo.Cache != nil {
f, err := rejectResticCache(repo)
if err != nil {
return nil, err
return nil, nil, err
}
fs = append(fs, f)
}
excludes = append(excludes, opts.Config.Excludes...)
// add patterns from file
if len(opts.ExcludeFiles) > 0 {
opts.Excludes = append(opts.Excludes, readExcludePatternsFromFiles(opts.ExcludeFiles)...)
excludes = append(excludes, readExcludePatternsFromFiles(opts.ExcludeFiles)...)
}
if len(opts.Excludes) > 0 {
fs = append(fs, rejectByPattern(opts.Excludes))
if len(excludes) > 0 {
fs = append(fs, rejectByPattern(excludes))
}
if opts.ExcludeCaches {
@ -224,13 +235,13 @@ func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets
for _, spec := range opts.ExcludeIfPresent {
f, err := rejectIfPresent(spec)
if err != nil {
return nil, err
return nil, nil, err
}
fs = append(fs, f)
}
return fs, nil
return fs, excludes, nil
}
// readExcludePatternsFromFiles reads all exclude files and returns the list of
@ -381,7 +392,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
}
// rejectFuncs collect functions that can reject items from the backup
rejectFuncs, err := collectRejectFuncs(opts, repo, targets)
rejectFuncs, excludes, err := collectRejectFuncs(opts, repo, targets)
if err != nil {
return err
}
@ -443,7 +454,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
}
snapshotOpts := archiver.SnapshotOptions{
Excludes: opts.Excludes,
Excludes: excludes,
Tags: opts.Tags,
Time: timeStamp,
Hostname: opts.Hostname,

View file

@ -3,7 +3,7 @@ package main
import (
"fmt"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/ui/options"
"github.com/spf13/cobra"
)

View file

@ -26,10 +26,11 @@ import (
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/fs"
"github.com/restic/restic/internal/limiter"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/textfile"
"github.com/restic/restic/internal/ui/config"
"github.com/restic/restic/internal/ui/options"
"github.com/restic/restic/internal/errors"
@ -40,8 +41,8 @@ var version = "compiled manually"
// GlobalOptions hold all global options for restic.
type GlobalOptions struct {
Repo string
PasswordFile string
config.Config
Quiet bool
Verbose int
NoLock bool
@ -86,8 +87,11 @@ func init() {
})
f := cmdRoot.PersistentFlags()
f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)")
// these fields are embedded in config.Config and queried via f.Get[...]()
f.StringP("repo", "r", "", "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
f.StringP("password-file", "p", "", "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)")
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level `n`)")
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
@ -233,7 +237,11 @@ func Exitf(exitcode int, format string, args ...interface{}) {
}
// resolvePassword determines the password to be used for opening the repository.
func resolvePassword(opts GlobalOptions, env string) (string, error) {
func resolvePassword(opts GlobalOptions) (string, error) {
if opts.Password != "" {
return opts.Password, nil
}
if opts.PasswordFile != "" {
s, err := textfile.Read(opts.PasswordFile)
if os.IsNotExist(errors.Cause(err)) {
@ -242,10 +250,6 @@ func resolvePassword(opts GlobalOptions, env string) (string, error) {
return strings.TrimSpace(string(s)), errors.Wrap(err, "Readfile")
}
if pwd := os.Getenv(env); pwd != "" {
return pwd, nil
}
return "", nil
}

View file

@ -9,10 +9,11 @@ import (
"runtime"
"testing"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/repository"
"github.com/restic/restic/internal/restic"
rtest "github.com/restic/restic/internal/test"
"github.com/restic/restic/internal/ui/config"
"github.com/restic/restic/internal/ui/options"
)
type dirEntry struct {
@ -209,7 +210,9 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
rtest.OK(t, os.MkdirAll(env.repo, 0700))
env.gopts = GlobalOptions{
Repo: env.repo,
Config: config.Config{
Repo: env.repo,
},
Quiet: true,
CacheDir: env.cache,
ctx: context.Background(),

View file

@ -390,14 +390,14 @@ func TestBackupExclude(t *testing.T) {
rtest.Assert(t, includes(files, "/testdata/foo.tar.gz"),
"expected file %q in first snapshot, but it's not included", "foo.tar.gz")
opts.Excludes = []string{"*.tar.gz"}
opts.Config.Excludes = []string{"*.tar.gz"}
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, env.gopts))
files = testRunLs(t, env.gopts, snapshotID)
rtest.Assert(t, !includes(files, "/testdata/foo.tar.gz"),
"expected file %q not in first snapshot, but it's included", "foo.tar.gz")
opts.Excludes = []string{"*.tar.gz", "private/secret"}
opts.Config.Excludes = []string{"*.tar.gz", "private/secret"}
testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts)
_, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, env.gopts))
files = testRunLs(t, env.gopts, snapshotID)

View file

@ -8,9 +8,11 @@ import (
"os"
"runtime"
"github.com/davecgh/go-spew/spew"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui/config"
"github.com/restic/restic/internal/ui/options"
"github.com/spf13/cobra"
@ -29,7 +31,24 @@ directories in an encrypted repository stored on different backends.
SilenceUsage: true,
DisableAutoGenTag: true,
PersistentPreRunE: func(c *cobra.Command, args []string) error {
PersistentPreRunE: func(c *cobra.Command, args []string) (err error) {
globalOptions.Config, err = config.Load("restic.conf")
if err != nil {
return err
}
err = config.ApplyEnv(&globalOptions.Config, os.Environ())
if err != nil {
return err
}
err = config.ApplyFlags(&globalOptions.Config, c.Flags())
if err != nil {
return err
}
spew.Dump(globalOptions.Config)
// set verbosity, default is one
globalOptions.verbosity = 1
if globalOptions.Quiet && (globalOptions.Verbose > 1) {
@ -54,7 +73,7 @@ directories in an encrypted repository stored on different backends.
if c.Name() == "version" {
return nil
}
pwd, err := resolvePassword(globalOptions, "RESTIC_PASSWORD")
pwd, err := resolvePassword(globalOptions)
if err != nil {
fmt.Fprintf(os.Stderr, "Resolving password failed: %v\n", err)
Exit(1)

View file

@ -5,7 +5,7 @@ import (
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/ui/options"
)
// Config contains all configuration necessary to connect to an azure compatible

View file

@ -6,7 +6,7 @@ import (
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/ui/options"
)
// Config contains all configuration necessary to connect to an b2 compatible

View file

@ -5,7 +5,7 @@ import (
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/ui/options"
)
// Config contains all configuration necessary to connect to a Google Cloud Storage

View file

@ -4,7 +4,7 @@ import (
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/ui/options"
)
// Config holds all information needed to open a local repository.

View file

@ -4,7 +4,7 @@ import (
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/ui/options"
)
// Config contains all configuration necessary to start rclone.

View file

@ -5,7 +5,7 @@ import (
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/ui/options"
)
// Config contains all configuration necessary to connect to a REST server.

View file

@ -6,7 +6,7 @@ import (
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/ui/options"
)
// Config contains all configuration necessary to connect to an s3 compatible

View file

@ -2,8 +2,9 @@ package backend
import (
"context"
"github.com/restic/restic/internal/errors"
"io"
"github.com/restic/restic/internal/errors"
)
// Semaphore limits access to a restricted resource.

View file

@ -6,7 +6,7 @@ import (
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/ui/options"
)
// Config collects all information required to connect to an sftp server.

View file

@ -5,7 +5,7 @@ import (
"strings"
"github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/ui/options"
)
// Config contains basic configuration needed to specify swift location for a swift server

View file

@ -0,0 +1,365 @@
package config
import (
"fmt"
"io/ioutil"
"reflect"
"strings"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/hcl/hcl/token"
"github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors"
"github.com/spf13/pflag"
)
// Config contains configuration items read from a file.
type Config struct {
Repo string `hcl:"repo" flag:"repo" env:"RESTIC_REPOSITORY"`
Password string `hcl:"password" env:"RESTIC_PASSWORD"`
PasswordFile string `hcl:"password_file" flag:"password-file" env:"RESTIC_PASSWORD_FILE"`
Backends map[string]Backend
Backup Backup `hcl:"backup"`
}
// Backend configures a backend.
type Backend struct {
Type string `hcl:"type"`
*BackendLocal `hcl:"-" json:"local"`
*BackendSFTP `hcl:"-" json:"sftp"`
}
// BackendLocal configures a local backend.
type BackendLocal struct {
Type string `hcl:"type"`
Path string `hcl:"path"`
}
// BackendSFTP configures an sftp backend.
type BackendSFTP struct {
Type string `hcl:"type"`
User string `hcl:"user"`
Host string `hcl:"host"`
Path string `hcl:"path"`
}
// Backup sets the options for the "backup" command.
type Backup struct {
Target []string `hcl:"target"`
Excludes []string `hcl:"exclude" flag:"exclude"`
}
// listTags returns the all the top-level tags with the name tagname of obj.
func listTags(obj interface{}, tagname string) map[string]struct{} {
list := make(map[string]struct{})
// resolve indirection if obj is a pointer
v := reflect.Indirect(reflect.ValueOf(obj))
for i := 0; i < v.NumField(); i++ {
f := v.Type().Field(i)
val := f.Tag.Get(tagname)
list[val] = struct{}{}
}
return list
}
func validateObjects(list *ast.ObjectList, validNames map[string]struct{}) error {
for _, item := range list.Items {
ident := item.Keys[0].Token.Value().(string)
if _, ok := validNames[ident]; !ok {
return errors.Errorf("unknown option %q found at line %v, column %v",
ident, item.Pos().Line, item.Pos().Column)
}
}
return nil
}
// Parse parses a config file from buf.
func Parse(buf []byte) (cfg Config, err error) {
parsed, err := hcl.ParseBytes(buf)
if err != nil {
return Config{}, err
}
err = hcl.DecodeObject(&cfg, parsed)
if err != nil {
return Config{}, err
}
root := parsed.Node.(*ast.ObjectList)
// load all 'backend' sections
cfg.Backends, err = parseBackends(root)
if err != nil {
return Config{}, err
}
// check for additional unknown items
rootTags := listTags(cfg, "hcl")
rootTags["backend"] = struct{}{}
checks := map[string]map[string]struct{}{
"": rootTags,
"backup": listTags(Backup{}, "hcl"),
}
for name, valid := range checks {
list := root
if name != "" {
if len(root.Filter(name).Items) == 0 {
continue
}
val := root.Filter(name).Items[0].Val
obj, ok := val.(*ast.ObjectType)
if !ok {
return Config{}, errors.Errorf("error in line %v, column %v: %q must be an object", val.Pos().Line, val.Pos().Column, name)
}
list = obj.List
}
err = validateObjects(list, valid)
if err != nil {
return Config{}, err
}
}
return cfg, nil
}
// parseBackends parses the backend configuration sections.
func parseBackends(root *ast.ObjectList) (map[string]Backend, error) {
backends := make(map[string]Backend)
// find top-level backend objects
for _, obj := range root.Items {
// is not an object block
if len(obj.Keys) == 0 {
continue
}
// does not start with an an identifier
if obj.Keys[0].Token.Type != token.IDENT {
continue
}
// something other than a backend section
if s, ok := obj.Keys[0].Token.Value().(string); !ok || s != "backend" {
continue
}
// missing name
if len(obj.Keys) != 2 {
return nil, errors.Errorf("backend has no name at line %v, column %v",
obj.Pos().Line, obj.Pos().Column)
}
// check that the name is not empty
name := obj.Keys[1].Token.Value().(string)
if len(name) == 0 {
return nil, errors.Errorf("backend name is empty at line %v, column %v",
obj.Pos().Line, obj.Pos().Column)
}
// decode object
var be Backend
err := hcl.DecodeObject(&be, obj)
if err != nil {
return nil, err
}
if be.Type == "" {
be.Type = "local"
}
var target interface{}
switch be.Type {
case "local":
be.BackendLocal = &BackendLocal{}
target = be.BackendLocal
case "sftp":
be.BackendSFTP = &BackendSFTP{}
target = be.BackendSFTP
default:
return nil, errors.Errorf("unknown backend type %q at line %v, column %v",
be.Type, obj.Pos().Line, obj.Pos().Column)
}
// check structure of the backend object
innerBlock, ok := obj.Val.(*ast.ObjectType)
if !ok {
return nil, errors.Errorf("unable to verify structure of backend %q at line %v, column %v",
name, obj.Pos().Line, obj.Pos().Column)
}
// check allowed types
err = validateObjects(innerBlock.List, listTags(target, "hcl"))
if err != nil {
return nil, err
}
err = hcl.DecodeObject(target, innerBlock)
if err != nil {
return nil, errors.Errorf("parsing backend %q (type %s) at line %v, column %v failed: %v",
name, be.Type, obj.Pos().Line, obj.Pos().Column, err)
}
if _, ok := backends[name]; ok {
return nil, errors.Errorf("backend %q at line %v, column %v already configured",
name, obj.Pos().Line, obj.Pos().Column)
}
backends[name] = be
}
return backends, nil
}
// Load loads a config from a file.
func Load(filename string) (Config, error) {
buf, err := ioutil.ReadFile(filename)
if err != nil {
return Config{}, err
}
return Parse(buf)
}
func getFieldsForTag(tagname string, target interface{}) map[string]reflect.Value {
v := reflect.ValueOf(target).Elem()
// resolve indirection
vi := reflect.Indirect(reflect.ValueOf(target))
attr := make(map[string]reflect.Value)
for i := 0; i < vi.NumField(); i++ {
typeField := vi.Type().Field(i)
tag := typeField.Tag.Get(tagname)
if tag == "" {
continue
}
field := v.FieldByName(typeField.Name)
if !field.CanSet() {
continue
}
attr[tag] = field
}
return attr
}
// ApplyFlags takes the values from the flag set and applies them to cfg.
func ApplyFlags(cfg interface{}, fset *pflag.FlagSet) error {
if reflect.TypeOf(cfg).Kind() != reflect.Ptr {
panic("target config is not a pointer")
}
debug.Log("apply flags")
attr := getFieldsForTag("flag", cfg)
var visitError error
fset.VisitAll(func(flag *pflag.Flag) {
if visitError != nil {
return
}
field, ok := attr[flag.Name]
if !ok {
return
}
if !flag.Changed {
return
}
debug.Log("apply flag %v, to field %v\n", flag.Name, field.Type().Name())
switch flag.Value.Type() {
case "count":
v, err := fset.GetCount(flag.Name)
if err != nil {
visitError = err
return
}
field.SetUint(uint64(v))
case "bool":
v, err := fset.GetBool(flag.Name)
if err != nil {
visitError = err
return
}
field.SetBool(v)
case "string":
v, err := fset.GetString(flag.Name)
if err != nil {
visitError = err
return
}
field.SetString(v)
case "stringArray":
v, err := fset.GetStringArray(flag.Name)
if err != nil {
visitError = err
return
}
slice := reflect.MakeSlice(reflect.TypeOf(v), len(v), len(v))
field.Set(slice)
for i, s := range v {
slice.Index(i).SetString(s)
}
default:
visitError = errors.Errorf("flag %v has unknown type %v", flag.Name, flag.Value.Type())
return
}
})
return visitError
}
// ApplyEnv takes the list of environment variables and applies them to the
// config.
func ApplyEnv(cfg interface{}, env []string) error {
attr := getFieldsForTag("env", cfg)
for _, s := range env {
data := strings.SplitN(s, "=", 2)
if len(data) != 2 {
continue
}
name, value := data[0], data[1]
field, ok := attr[name]
if !ok {
continue
}
if field.Kind() != reflect.String {
panic(fmt.Sprintf("unsupported field type %v", field.Kind()))
}
debug.Log("apply env %v (%q) to %v\n", name, value, field.Type().Name())
field.SetString(value)
}
return nil
}
// ApplyOptions takes a list of Options and applies them to the config.
func ApplyOptions(cfg interface{}, opts map[string]string) error {
return errors.New("not implemented")
}

View file

@ -0,0 +1,250 @@
package config
import (
"encoding/json"
"flag"
"io/ioutil"
"path/filepath"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/spf13/pflag"
)
var updateGoldenFiles = flag.Bool("update", false, "update golden files in testdata/")
func saveGoldenFile(t testing.TB, base string, cfg Config) {
buf, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
t.Fatalf("error marshaling result: %v", err)
}
buf = append(buf, '\n')
if err = ioutil.WriteFile(filepath.Join("testdata", base+".golden"), buf, 0644); err != nil {
t.Fatalf("unable to update golden file: %v", err)
}
}
func loadGoldenFile(t testing.TB, base string) Config {
buf, err := ioutil.ReadFile(filepath.Join("testdata", base+".golden"))
if err != nil {
t.Fatal(err)
}
var cfg Config
err = json.Unmarshal(buf, &cfg)
if err != nil {
t.Fatal(err)
}
return cfg
}
func TestConfigLoad(t *testing.T) {
entries, err := ioutil.ReadDir("testdata")
if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
filename := entry.Name()
if filepath.Ext(filename) != ".conf" {
continue
}
base := strings.TrimSuffix(filename, ".conf")
t.Run(base, func(t *testing.T) {
cfg, err := Load(filepath.Join("testdata", filename))
if err != nil {
t.Fatal(err)
}
if *updateGoldenFiles {
saveGoldenFile(t, base, cfg)
}
want := loadGoldenFile(t, base)
if !cmp.Equal(want, cfg) {
t.Errorf("wrong config: %v", cmp.Diff(want, cfg))
}
})
}
}
func TestInvalidConfigs(t *testing.T) {
var tests = []struct {
config string
err string
}{
{
config: `backend ""`,
err: "expected start of object",
},
{
config: `backend "" {}`,
err: "name is empty",
},
{
config: `backend "foo" {
type = ""
user = "xxx"
}`,
err: `unknown option "user"`,
},
{
config: `backend "foo" {
type = "local"
user = "xxx"
}`,
err: `unknown option "user"`,
},
{
config: `backend "foo" {
path = "/foo"
}
backend "foo" {
path = "/bar"
}`,
err: `backend "foo" already configured`,
},
{
config: `backend "foo" {
type = "xxx"
}`,
err: `unknown backend type "xxx"`,
},
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
_, err := Parse([]byte(test.config))
if err == nil {
t.Fatalf("expected error not found, got nil")
}
if !strings.Contains(err.Error(), test.err) {
t.Fatalf("returned error does not contain substring %q: %q", test.err, err.Error())
}
})
}
}
func TestConfigApplyFlags(t *testing.T) {
var tests = []struct {
filename string
applyFlags func(cfg *Config) error
want Config
}{
{
filename: "backup.conf",
applyFlags: func(cfg *Config) error {
args := []string{"--exclude", "foo/*.go"}
s := pflag.NewFlagSet("", pflag.ContinueOnError)
s.StringArrayP("exclude", "e", nil, "exclude files")
err := s.Parse(args)
if err != nil {
return err
}
return ApplyFlags(&cfg.Backup, s)
},
want: Config{
Backup: Backup{
Target: []string{"foo", "/home/user"},
Excludes: []string{"foo/*.go"},
},
Backends: map[string]Backend{},
},
},
{
filename: "backup.conf",
applyFlags: func(cfg *Config) error {
args := []string{"--repo", "sftp:user@server:/srv/backup/repo"}
s := pflag.NewFlagSet("", pflag.ContinueOnError)
s.StringP("repo", "r", "", "repository to backup to or restore from")
err := s.Parse(args)
if err != nil {
return err
}
return ApplyFlags(cfg, s)
},
want: Config{
Backup: Backup{
Target: []string{"foo", "/home/user"},
},
Repo: "sftp:user@server:/srv/backup/repo",
Backends: map[string]Backend{},
},
},
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
cfg, err := Load(filepath.Join("testdata", test.filename))
if err != nil {
t.Fatal(err)
}
err = test.applyFlags(&cfg)
if err != nil {
t.Fatal(err)
}
if !cmp.Equal(test.want, cfg) {
t.Error(cmp.Diff(test.want, cfg))
}
})
}
}
func TestConfigApplyEnv(t *testing.T) {
var tests = []struct {
filename string
env []string
want Config
}{
{
filename: "backup.conf",
env: []string{
"RESTIC_REPOSITORY=/tmp/repo",
"RESTIC_PASSWORD=foobar",
"RESTIC_PASSWORD_FILE=/root/secret.txt",
},
want: Config{
Password: "foobar",
PasswordFile: "/root/secret.txt",
Repo: "/tmp/repo",
Backup: Backup{
Target: []string{"foo", "/home/user"},
},
Backends: map[string]Backend{},
},
},
}
for _, test := range tests {
t.Run("", func(t *testing.T) {
cfg, err := Load(filepath.Join("testdata", test.filename))
if err != nil {
t.Fatal(err)
}
err = ApplyEnv(&cfg, test.env)
if err != nil {
t.Fatal(err)
}
if !cmp.Equal(test.want, cfg) {
t.Error(cmp.Diff(test.want, cfg))
}
})
}
}

28
internal/ui/config/testdata/all.conf vendored Normal file
View file

@ -0,0 +1,28 @@
repo = "sftp:user@server:/srv/repo"
password = "secret"
password_file = "/root/secret.txt"
backup {
target = [
"/home/user/",
"/home/otheruser",
]
exclude = ["*.c"]
}
backend "local" {
type = "local"
path = "/foo/bar"
}
backend "local2" {
path = "/foo/bar"
}
backend "sftp" {
type = "sftp"
user = "foo"
host = "bar"
path = "/foo/bar"
}

42
internal/ui/config/testdata/all.golden vendored Normal file
View file

@ -0,0 +1,42 @@
{
"Repo": "sftp:user@server:/srv/repo",
"Password": "secret",
"PasswordFile": "/root/secret.txt",
"Backends": {
"local": {
"Type": "local",
"local": {
"Type": "local",
"Path": "/foo/bar"
},
"sftp": null
},
"local2": {
"Type": "local",
"local": {
"Type": "",
"Path": "/foo/bar"
},
"sftp": null
},
"sftp": {
"Type": "sftp",
"local": null,
"sftp": {
"Type": "sftp",
"User": "foo",
"Host": "bar",
"Path": "/foo/bar"
}
}
},
"Backup": {
"Target": [
"/home/user/",
"/home/otheruser"
],
"Excludes": [
"*.c"
]
}
}

View file

@ -0,0 +1,10 @@
password = "geheim"
backend "foo" {
type = "local"
path = "/srv/data/repo"
}
backend "bar" {
path = "/srv/data/repo"
}

View file

@ -0,0 +1,27 @@
{
"Repo": "",
"Password": "geheim",
"PasswordFile": "",
"Backends": {
"bar": {
"Type": "local",
"local": {
"Type": "",
"Path": "/srv/data/repo"
},
"sftp": null
},
"foo": {
"Type": "local",
"local": {
"Type": "local",
"Path": "/srv/data/repo"
},
"sftp": null
}
},
"Backup": {
"Target": null,
"Excludes": null
}
}

View file

@ -0,0 +1,6 @@
backup {
target = [
"foo",
"/home/user",
]
}

View file

@ -0,0 +1,13 @@
{
"Repo": "",
"Password": "",
"PasswordFile": "",
"Backends": {},
"Backup": {
"Target": [
"foo",
"/home/user"
],
"Excludes": null
}
}

View file

@ -0,0 +1,6 @@
backend "test" {
type = "local"
path = "/foo/bar/baz"
}
repo = "test"

View file

@ -0,0 +1,19 @@
{
"Repo": "test",
"Password": "",
"PasswordFile": "",
"Backends": {
"test": {
"Type": "local",
"local": {
"Type": "local",
"Path": "/foo/bar/baz"
},
"sftp": null
}
},
"Backup": {
"Target": null,
"Excludes": null
}
}