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") }