From aaef54559a0cbdcefe9155e9c676a9b6b3f5cc7b Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Fri, 4 May 2018 00:15:15 +0200 Subject: [PATCH] wip --- cmd/restic/global.go | 9 +- cmd/restic/integration_helpers_test.go | 5 +- cmd/restic/main.go | 18 +- internal/ui/config/config.go | 198 +++++++++++++++--- internal/ui/config/testdata/backup.conf | 6 + internal/ui/config/testdata/backup.golden | 10 + internal/ui/config/testdata/quiet.conf | 1 - internal/ui/config/testdata/quiet.golden | 4 - internal/ui/config/testdata/repo_local.conf | 8 +- internal/ui/config/testdata/repo_local.golden | 7 +- 10 files changed, 224 insertions(+), 42 deletions(-) create mode 100644 internal/ui/config/testdata/backup.conf create mode 100644 internal/ui/config/testdata/backup.golden delete mode 100644 internal/ui/config/testdata/quiet.conf delete mode 100644 internal/ui/config/testdata/quiet.golden diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 96d1f9e36..4c662d742 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -30,6 +30,7 @@ import ( "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/errors" @@ -40,7 +41,8 @@ var version = "compiled manually" // GlobalOptions hold all global options for restic. type GlobalOptions struct { - Repo string + config.Config + PasswordFile string Quiet bool Verbose int @@ -86,7 +88,10 @@ 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)") + + // 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.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "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`)") diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index d0450817d..7b04becef 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -13,6 +13,7 @@ import ( "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" ) 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(), diff --git a/cmd/restic/main.go b/cmd/restic/main.go index 01a902b1d..3b3a62028 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -11,6 +11,7 @@ import ( "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/spf13/cobra" @@ -29,7 +30,22 @@ 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 + } + // set verbosity, default is one globalOptions.verbosity = 1 if globalOptions.Quiet && (globalOptions.Verbose > 1) { diff --git a/internal/ui/config/config.go b/internal/ui/config/config.go index 33df5ca1b..fab783d38 100644 --- a/internal/ui/config/config.go +++ b/internal/ui/config/config.go @@ -2,25 +2,34 @@ package config import ( "fmt" + "io/ioutil" "reflect" + "strings" - "github.com/davecgh/go-spew/spew" "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" ) -// Repo is a configured repository -type Repo struct { +// Config contains configuration items read from a file. +type Config struct { + Repo string `hcl:"repo" flag:"repo" env:"RESTIC_REPOSITORY"` + + Backends map[string]Backend `hcl:"backend"` + Backup *Backup `hcl:"backup"` +} + +// Backend is a configured backend to store a repository. +type Backend struct { Backend string Path string } -// Config contains configuration items read from a file. -type Config struct { - Quiet bool `hcl:"quiet"` - Repos map[string]Repo `hcl:"repo"` +// Backup sets the options for the "backup" command. +type Backup struct { + Target []string `hcl:"target"` } // listTags returns the all the top-level tags with the name tagname of obj. @@ -40,6 +49,18 @@ func listTags(obj interface{}, tagname string) map[string]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) @@ -52,25 +73,154 @@ func Parse(buf []byte) (cfg Config, err error) { return Config{}, err } - // check for additional top-level items - validNames := listTags(cfg, "hcl") - for _, item := range parsed.Node.(*ast.ObjectList).Items { - fmt.Printf("-----------\n") - spew.Dump(item) - var ident string - for _, key := range item.Keys { - if key.Token.Type == token.IDENT { - ident = key.Token.Text - } - } - fmt.Printf("ident is %q\n", ident) + // check for additional unknown items + root := parsed.Node.(*ast.ObjectList) - if _, ok := validNames[ident]; !ok { - return Config{}, errors.Errorf("unknown option %q found at line %v, column %v: %v", - ident, item.Pos().Line, item.Pos().Column) + checks := map[string]map[string]struct{}{ + "": listTags(cfg, "hcl"), + "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 } } - // spew.Dump(cfg) return cfg, 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") + } + + 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) + 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 +} diff --git a/internal/ui/config/testdata/backup.conf b/internal/ui/config/testdata/backup.conf new file mode 100644 index 000000000..49d4136d0 --- /dev/null +++ b/internal/ui/config/testdata/backup.conf @@ -0,0 +1,6 @@ +backup { + target = [ + "foo", + "/home/user", + ] +} diff --git a/internal/ui/config/testdata/backup.golden b/internal/ui/config/testdata/backup.golden new file mode 100644 index 000000000..74155a7d9 --- /dev/null +++ b/internal/ui/config/testdata/backup.golden @@ -0,0 +1,10 @@ +{ + "Backends": null, + "Repo": "", + "Backup": { + "Target": [ + "foo", + "/home/user" + ] + } +} diff --git a/internal/ui/config/testdata/quiet.conf b/internal/ui/config/testdata/quiet.conf deleted file mode 100644 index 986554250..000000000 --- a/internal/ui/config/testdata/quiet.conf +++ /dev/null @@ -1 +0,0 @@ -quiet = true diff --git a/internal/ui/config/testdata/quiet.golden b/internal/ui/config/testdata/quiet.golden deleted file mode 100644 index 015f7223a..000000000 --- a/internal/ui/config/testdata/quiet.golden +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Quiet": true, - "Repos": null -} diff --git a/internal/ui/config/testdata/repo_local.conf b/internal/ui/config/testdata/repo_local.conf index 30230564c..0fecdbad2 100644 --- a/internal/ui/config/testdata/repo_local.conf +++ b/internal/ui/config/testdata/repo_local.conf @@ -1,10 +1,6 @@ -repo "test" { +backend "test" { backend = "local" path = "/foo/bar/baz" } -foobar "test" { - x = "y" -} - -quiet = false +repo = "test" diff --git a/internal/ui/config/testdata/repo_local.golden b/internal/ui/config/testdata/repo_local.golden index 6136f7a5a..6940b3eaf 100644 --- a/internal/ui/config/testdata/repo_local.golden +++ b/internal/ui/config/testdata/repo_local.golden @@ -1,9 +1,10 @@ { - "Quiet": false, - "Repos": { + "Backends": { "test": { "Backend": "local", "Path": "/foo/bar/baz" } - } + }, + "Repo": "test", + "Backup": null }