forked from TrueCloudLab/restic
Compare commits
16 commits
master
...
add-config
Author | SHA1 | Date | |
---|---|---|---|
|
abb1dc4eb6 | ||
|
8d21bb92db | ||
|
0b3c402801 | ||
|
b3b70002ab | ||
|
4916ba7a8a | ||
|
ea565df3e8 | ||
|
0758c92afc | ||
|
8b0092908a | ||
|
ffd7bc1021 | ||
|
6bad560324 | ||
|
7ad648c686 | ||
|
0c078cc205 | ||
|
1fbcf63830 | ||
|
740e2d6139 | ||
|
aaef54559a | ||
|
722517c480 |
28 changed files with 845 additions and 41 deletions
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
365
internal/ui/config/config.go
Normal file
365
internal/ui/config/config.go
Normal 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")
|
||||
}
|
250
internal/ui/config/config_test.go
Normal file
250
internal/ui/config/config_test.go
Normal 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
28
internal/ui/config/testdata/all.conf
vendored
Normal 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
42
internal/ui/config/testdata/all.golden
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
10
internal/ui/config/testdata/backend-local.conf
vendored
Normal file
10
internal/ui/config/testdata/backend-local.conf
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
password = "geheim"
|
||||
|
||||
backend "foo" {
|
||||
type = "local"
|
||||
path = "/srv/data/repo"
|
||||
}
|
||||
|
||||
backend "bar" {
|
||||
path = "/srv/data/repo"
|
||||
}
|
27
internal/ui/config/testdata/backend-local.golden
vendored
Normal file
27
internal/ui/config/testdata/backend-local.golden
vendored
Normal 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
|
||||
}
|
||||
}
|
6
internal/ui/config/testdata/backup.conf
vendored
Normal file
6
internal/ui/config/testdata/backup.conf
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
backup {
|
||||
target = [
|
||||
"foo",
|
||||
"/home/user",
|
||||
]
|
||||
}
|
13
internal/ui/config/testdata/backup.golden
vendored
Normal file
13
internal/ui/config/testdata/backup.golden
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"Repo": "",
|
||||
"Password": "",
|
||||
"PasswordFile": "",
|
||||
"Backends": {},
|
||||
"Backup": {
|
||||
"Target": [
|
||||
"foo",
|
||||
"/home/user"
|
||||
],
|
||||
"Excludes": null
|
||||
}
|
||||
}
|
6
internal/ui/config/testdata/repo_local.conf
vendored
Normal file
6
internal/ui/config/testdata/repo_local.conf
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
backend "test" {
|
||||
type = "local"
|
||||
path = "/foo/bar/baz"
|
||||
}
|
||||
|
||||
repo = "test"
|
19
internal/ui/config/testdata/repo_local.golden
vendored
Normal file
19
internal/ui/config/testdata/repo_local.golden
vendored
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue