From 92933120e2b7776b5e9e4cb8d5e66246c5aeb4bc Mon Sep 17 00:00:00 2001 From: klizhentas Date: Fri, 30 Oct 2015 16:57:57 -0700 Subject: [PATCH] re-vendor configure and update code --- Godeps/Godeps.json | 2 +- .../gravitational/configure/README.md | 55 +- .../github.com/gravitational/configure/cli.go | 3 +- .../configure/{ => cstrings}/split.go | 17 +- .../configure/{ => cstrings}/split_test.go | 6 +- .../github.com/gravitational/configure/kv.go | 12 +- .../configure/schema/decoding.go | 96 +++ .../gravitational/configure/schema/schema.go | 669 ++++++++++++++++++ .../configure/schema/schema_test.go | 487 +++++++++++++ lib/service/cfg.go | 5 +- lib/service/tpl.go | 4 +- 11 files changed, 1329 insertions(+), 27 deletions(-) rename Godeps/_workspace/src/github.com/gravitational/configure/{ => cstrings}/split.go (80%) rename Godeps/_workspace/src/github.com/gravitational/configure/{ => cstrings}/split_test.go (92%) create mode 100644 Godeps/_workspace/src/github.com/gravitational/configure/schema/decoding.go create mode 100644 Godeps/_workspace/src/github.com/gravitational/configure/schema/schema.go create mode 100644 Godeps/_workspace/src/github.com/gravitational/configure/schema/schema_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 860788b9353..eda7ce262b6 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -67,7 +67,7 @@ }, { "ImportPath": "github.com/gravitational/configure", - "Rev": "725b3af8847feced5316be064a297d7c8a1052b3" + "Rev": "af81ee7530fe68dbac8824c44f9fd0b0546c3efb" }, { "ImportPath": "github.com/gravitational/form", diff --git a/Godeps/_workspace/src/github.com/gravitational/configure/README.md b/Godeps/_workspace/src/github.com/gravitational/configure/README.md index 25d6c78d622..bd591ba3ada 100644 --- a/Godeps/_workspace/src/github.com/gravitational/configure/README.md +++ b/Godeps/_workspace/src/github.com/gravitational/configure/README.md @@ -1,24 +1,43 @@ # Configure -Package configure generates configuration tools based on a struct -definition with tags. It can read a configuration for a struct -from YAML, environment variables and command line. +`configure` is a golang library that populates a struct from environment variables, command line arugments and YAML files. +It works by reading a struct definition with special tags. -```go -// Given the struct definition: - type Config struct { - StringVar string `env:"TEST_STRING_VAR" cli:"string" yaml:"string"` - BoolVar bool `env:"TEST_BOOL_VAR" cli:"bool" yaml:"bool"` - IntVar int `env:"TEST_INT_VAR" cli:"int" yaml:"int"` - HexVar hexType `env:"TEST_HEX_VAR" cli:"hex" yaml:"hex"` - MapVar map[string]string `env:"TEST_MAP_VAR" cli:"map" yaml:"map,flow"` - SliceMapVar []map[string]string `env:"TEST_SLICE_MAP_VAR" cli:"slice" yaml:"slice,flow"` - } +### Usage + +The latest can be seen if you run ``` - -You can start initializing the struct from YAML, command line or environment. - -```shell -# use godoc for more details godoc github.com/gravitational/configure ``` + +But here's a quickstart: Define a sample structure, for examlpe: +```go + + type Config struct { + StringVar string `env:"STRING_VAR" cli:"string-var" yaml:"string_var"` + BoolVar bool `env:"BOOL_VAR" cli:"bool_var" yaml:"bool_var"` + IntVar int `env:"INT_VAR" cli:"int_var" yaml:"int_var"` + HexVar hexType `env:"HEX_VAR" cli:"hex_var" yaml:"hex_var"` + MapVar map[string]string `env:"MAP_VAR" cli:"map_var" yaml:"map_var,flow"` + SliceMapVar []map[string]string `env:"SLICE_MAP_VAR" cli:"slice_var" yaml:"slice_var,flow"` + } +``` + +Then you can query the environment and populate that structure from environment variables, YAML files or command line arguments. + +```go + import ( + "os" + "github.com/gravitational/configure" + ) + + func main() { + var cfg Config + // parse environment variables + err := configure.ParseEnv(&cfg) + // parse YAML + err = configure.ParseYAML(&cfg) + // parse command line arguments + err = configure.ParseCommandLine(&cfg, os.Ars[1:]) + } +``` diff --git a/Godeps/_workspace/src/github.com/gravitational/configure/cli.go b/Godeps/_workspace/src/github.com/gravitational/configure/cli.go index fe69df129bc..18ae79e5515 100644 --- a/Godeps/_workspace/src/github.com/gravitational/configure/cli.go +++ b/Godeps/_workspace/src/github.com/gravitational/configure/cli.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/configure/cstrings" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/trace" "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/alecthomas/kingpin.v2" ) @@ -139,7 +140,7 @@ func setMap(kv *map[string]string, val string) error { if len(*kv) == 0 { *kv = make(map[string]string) } - for _, i := range SplitComma(val) { + for _, i := range cstrings.SplitComma(val) { vals := strings.SplitN(i, ":", 2) if len(vals) != 2 { return trace.Errorf("extra options should be defined like KEY:VAL") diff --git a/Godeps/_workspace/src/github.com/gravitational/configure/split.go b/Godeps/_workspace/src/github.com/gravitational/configure/cstrings/split.go similarity index 80% rename from Godeps/_workspace/src/github.com/gravitational/configure/split.go rename to Godeps/_workspace/src/github.com/gravitational/configure/cstrings/split.go index 220c4745186..76fc7644ba9 100644 --- a/Godeps/_workspace/src/github.com/gravitational/configure/split.go +++ b/Godeps/_workspace/src/github.com/gravitational/configure/cstrings/split.go @@ -1,4 +1,4 @@ -package configure +package cstrings import ( "bufio" @@ -29,6 +29,21 @@ func Split(delim, escape rune, v string) []string { return out } +// SplitAt splits array of strings at a given separator +func SplitAt(args []string, sep string) ([]string, []string) { + index := -1 + for i, a := range args { + if a == sep { + index = i + break + } + } + if index == -1 { + return args, []string{} + } + return args[:index], args[index+1:] +} + type splitter struct { delim rune escape rune diff --git a/Godeps/_workspace/src/github.com/gravitational/configure/split_test.go b/Godeps/_workspace/src/github.com/gravitational/configure/cstrings/split_test.go similarity index 92% rename from Godeps/_workspace/src/github.com/gravitational/configure/split_test.go rename to Godeps/_workspace/src/github.com/gravitational/configure/cstrings/split_test.go index e3a9be8aa5a..098a1abf8ae 100644 --- a/Godeps/_workspace/src/github.com/gravitational/configure/split_test.go +++ b/Godeps/_workspace/src/github.com/gravitational/configure/cstrings/split_test.go @@ -1,9 +1,13 @@ -package configure +package cstrings import ( + "testing" + . "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/check.v1" ) +func TestStrings(t *testing.T) { TestingT(t) } + type USuite struct { } diff --git a/Godeps/_workspace/src/github.com/gravitational/configure/kv.go b/Godeps/_workspace/src/github.com/gravitational/configure/kv.go index 6f3bafa5861..f0e1c0dd8a2 100644 --- a/Godeps/_workspace/src/github.com/gravitational/configure/kv.go +++ b/Godeps/_workspace/src/github.com/gravitational/configure/kv.go @@ -6,7 +6,9 @@ import ( "fmt" "strings" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/configure/cstrings" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/trace" + "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/alecthomas/kingpin.v2" ) // KeyVal is map that can parse itself from string, represented as a @@ -18,7 +20,7 @@ func (kv *KeyVal) Set(v string) error { if len(*kv) == 0 { *kv = make(map[string]string) } - for _, i := range SplitComma(v) { + for _, i := range cstrings.SplitComma(v) { vals := strings.SplitN(i, ":", 2) if len(vals) != 2 { return trace.Errorf("extra options should be defined like KEY:VAL") @@ -47,6 +49,14 @@ func (kv *KeyVal) String() string { return b.String() } +// KeyValParam accepts a kingpin setting parameter and returns +// kingpin-compatible value +func KeyValParam(s kingpin.Settings) *KeyVal { + kv := make(KeyVal) + s.SetValue(&kv) + return &kv +} + // KeyValSlice is a list of key value strings type KeyValSlice []map[string]string diff --git a/Godeps/_workspace/src/github.com/gravitational/configure/schema/decoding.go b/Godeps/_workspace/src/github.com/gravitational/configure/schema/decoding.go new file mode 100644 index 00000000000..0f2fc3e8c1d --- /dev/null +++ b/Godeps/_workspace/src/github.com/gravitational/configure/schema/decoding.go @@ -0,0 +1,96 @@ +package schema + +import ( + "encoding/json" + "strings" +) + +type configV1 struct { + Params []paramSpec `json:"params"` +} + +type paramSpec struct { + Name string `json:"name"` + Description string `json:"description"` + Type string `json:"type"` + Check string `json:"check"` + Default string `json:"default"` + CLI cliSpec `json:"cli"` // cli-specific settings + Env string `json:"env"` // environment variable name + Required bool `json:"required"` + S json.RawMessage `json:"spec"` +} + +func (p *paramSpec) common() paramCommon { + return paramCommon{ + name: p.Name, + descr: p.Description, + check: p.Check, + def: p.Default, + cli: p.CLI, + req: p.Required, + env: p.Env, + } +} + +type paramCommon struct { + name string + descr string + check string + req bool + cli cliSpec + def string + env string +} + +func (p *paramCommon) EnvName() string { + if p.env != "" { + return p.env + } + return strings.ToUpper(p.name) +} + +func (p *paramCommon) CLIName() string { + if p.cli.Name != "" { + return p.cli.Name + } + return p.name +} + +func (p *paramCommon) Name() string { + return p.name +} + +func (p *paramCommon) Description() string { + return p.descr +} + +func (p *paramCommon) Check() string { + return p.check +} + +func (p *paramCommon) Required() bool { + return p.req +} + +func (p *paramCommon) Default() string { + return p.def +} + +type cliSpec struct { + Name string `json:"name"` + Type string `json:"type"` // type is either 'flag' or 'arg', 'flag is the default' +} + +func (s *paramSpec) Spec() paramSpec { + return *s +} + +type kvSpec struct { + Separator string `json:"separator"` + Keys []paramSpec `json:"keys"` +} + +type enumSpec struct { + Values []string `json:"values"` +} diff --git a/Godeps/_workspace/src/github.com/gravitational/configure/schema/schema.go b/Godeps/_workspace/src/github.com/gravitational/configure/schema/schema.go new file mode 100644 index 00000000000..5c1487999de --- /dev/null +++ b/Godeps/_workspace/src/github.com/gravitational/configure/schema/schema.go @@ -0,0 +1,669 @@ +package schema + +import ( + "encoding/json" + "fmt" + "go/ast" + "go/parser" + "io" + "strconv" + "strings" + + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/configure/cstrings" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/trace" + "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/alecthomas/kingpin.v2" +) + +func ParseJSON(r io.Reader) (*Config, error) { + var c *configV1 + + if err := json.NewDecoder(r).Decode(&c); err != nil { + return nil, trace.Wrap(err) + } + return newParser().parse(*c) +} + +func ParseVariablesJSON(r io.Reader) (*Config, error) { + var variables []paramSpec + + if err := json.NewDecoder(r).Decode(&variables); err != nil { + return nil, trace.Wrap(err) + } + return newParser().parse(configV1{Params: variables}) +} + +type Config struct { + Params []Param +} + +func (c *Config) Vars() map[string]string { + vars := make(map[string]string, len(c.Params)) + for _, p := range c.Params { + k, v := p.Vars() + vars[k] = v + } + return vars +} + +func (c *Config) EnvVars() map[string]string { + vars := make(map[string]string, len(c.Params)) + for _, p := range c.Params { + k, v := p.EnvVars() + vars[k] = v + } + return vars +} + +func (c *Config) Args() []string { + args := []string{} + for _, p := range c.Params { + args = append(args, p.Args()...) + } + return args +} + +func (c *Config) ParseArgs(args []string) error { + app := cliApp(c, false) + _, err := app.Parse(args) + return err +} + +func (c *Config) ParseEnv() error { + app := cliApp(c, true) + _, err := app.Parse([]string{}) + return err +} + +func (c *Config) ParseVars(vars map[string]string) error { + for _, p := range c.Params { + val, ok := vars[p.Name()] + if !ok { + if p.Required() { + return trace.Errorf("missing value for required variable: %v", p.Name()) + } else { + val = p.Default() + } + } + if err := p.Set(val); err != nil { + return trace.Wrap(err) + } + } + return nil +} + +func cliApp(c *Config, useEnv bool) *kingpin.Application { + app := kingpin.New("app", "Orbit package configuration tool") + + for _, p := range c.Params { + cliFlag(app, p, useEnv) + } + return app +} + +func cliFlag(app *kingpin.Application, p Param, useEnv bool) { + name := p.CLIName() + f := app.Flag(name, p.Description()) + if p.Required() { + f = f.Required() + } + if p.Default() != "" { + f = f.Default(p.Default()) + } + if useEnv { + f.OverrideDefaultFromEnvar(p.EnvName()) + } + SetParam(p, f) +} + +func SetParam(p Param, s kingpin.Settings) { + s.SetValue(p) +} + +type Param interface { + Name() string + CLIName() string + Description() string + Check() string + Required() bool + Default() string + + // New returns a new instance of the param identical to this + New() Param + + // Set is required to set parameters from command line string + Set(string) error + // String is required to output value to command line string + String() string + + // Args returns argument strings in cli format + Args() []string + + // Values returns a tuple with environment variable name and value + EnvVars() (string, string) + + // Vars returns a tuple with the variable name and value + Vars() (string, string) + + EnvName() string +} + +func newParser() *cparser { + return &cparser{ + params: []Param{}, + } +} + +type cparser struct { + params []Param +} + +func (p *cparser) parse(c configV1) (*Config, error) { + cfg := &Config{ + Params: make([]Param, len(c.Params)), + } + // parse types + if len(c.Params) != 0 { + for i, ts := range c.Params { + pr, err := p.parseParam(ts, false) + if err != nil { + return nil, err + } + cfg.Params[i] = pr + } + } + + return cfg, nil +} + +func (p *cparser) parseParam(s paramSpec, scalar bool) (Param, error) { + if s.Name == "" { + return nil, trace.Errorf("set a type name") + } + if err := p.checkName(s.Name); err != nil { + return nil, err + } + if s.Type == "" { + return nil, trace.Errorf("set a type for '%v'", s.Name) + } + switch s.Type { + case "String": + pr := &StringParam{} + pr.paramCommon = s.common() + return pr, nil + case "Path": + pr := &PathParam{} + pr.paramCommon = s.common() + return pr, nil + case "Int": + pr := &IntParam{} + pr.paramCommon = s.common() + return pr, nil + case "Bool": + pr := &BoolParam{} + pr.paramCommon = s.common() + return pr, nil + case "KeyVal": + return p.parseKeyVal(s) + case "Enum": + return p.parseEnum(s) + case "List": + if scalar { + return nil, trace.Errorf( + "scalar values are not allowed here: '%v'", s.Type) + } + return p.parseList(s) + } + return nil, trace.Errorf("unrecognized type: '%v'", s.Type) +} + +func (p *cparser) parseList(s paramSpec) (Param, error) { + var ps *paramSpec + if err := json.Unmarshal(s.S, &ps); err != nil { + return nil, trace.Wrap(err, "failed to parse: '%v'", string(s.S)) + } + el, err := p.parseParam(*ps, false) + if err != nil { + return nil, err + } + l := &ListParam{el: el} + l.paramCommon = s.common() + return l, nil +} + +func (p *cparser) parseEnum(s paramSpec) (Param, error) { + var e *enumSpec + if err := json.Unmarshal(s.S, &e); err != nil { + return nil, trace.Wrap( + err, fmt.Sprintf("failed to parse: '%v'", string(s.S))) + } + if len(e.Values) == 0 { + return nil, trace.Errorf("provide at least one value for '%v'", s.Name) + } + + values := make([]string, len(e.Values)) + seen := make(map[string]bool, len(e.Values)) + + for i, v := range e.Values { + if v == "" { + return nil, trace.Errorf("value can not be an empty string") + } + if seen[v] { + return nil, trace.Errorf("duplicate value: '%v'", v) + } + values[i] = v + } + + ep := &EnumParam{values: values} + ep.paramCommon = s.common() + return ep, nil +} + +func (p *cparser) parseKeyVal(s paramSpec) (Param, error) { + var k *kvSpec + if err := json.Unmarshal(s.S, &k); err != nil { + return nil, trace.Wrap( + err, fmt.Sprintf("failed to parse: '%v'", string(s.S))) + } + if len(k.Keys) == 0 { + return nil, trace.Errorf("provide at least one key for '%v'", s.Name) + } + + keys := make([]Param, len(k.Keys)) + + for i, ks := range k.Keys { + k, err := p.parseParam(ks, true) + if err != nil { + return nil, err + } + keys[i] = k + } + + if err := checkSameNames(keys); err != nil { + return nil, err + } + + kv := &KVParam{keys: keys, separator: k.Separator} + kv.paramCommon = s.common() + return kv, nil +} + +func (p *cparser) checkName(n string) error { + for _, pr := range p.params { + if pr.Name() == n { + return trace.Errorf("parameter '%v' is already defined", n) + } + } + e, err := parser.ParseExpr(n) + if err != nil { + return trace.Wrap( + err, fmt.Sprintf("failed to parse name: '%v'", n)) + } + if _, ok := e.(*ast.Ident); !ok { + return trace.Wrap( + err, fmt.Sprintf("name should be a valid identifier: '%v'", n)) + } + return nil +} + +func checkSameNames(ps []Param) error { + n := map[string]bool{} + for _, p := range ps { + if n[p.Name()] { + return trace.Errorf("parameter '%v' is already defined", n) + } + n[p.Name()] = true + } + return nil +} + +type PathParam struct { + paramCommon + val *string +} + +func (p *PathParam) New() Param { + return &PathParam{p.paramCommon, nil} +} + +func (p *PathParam) Args() []string { + return []string{fmt.Sprintf("--%v", p.CLIName()), p.String()} +} + +func (p *PathParam) EnvVars() (string, string) { + return p.EnvName(), p.String() +} + +func (p *PathParam) Vars() (string, string) { + return p.Name(), p.String() +} + +func (p *PathParam) Set(s string) error { + p.val = &s + return nil +} + +func (p *PathParam) String() string { + if p.val == nil { + return p.Default() + } + return *p.val +} + +type StringParam struct { + paramCommon + val *string +} + +func (p *StringParam) New() Param { + return &StringParam{p.paramCommon, nil} +} + +func (p *StringParam) Set(s string) error { + p.val = &s + return nil +} + +func (p *StringParam) String() string { + if p.val == nil { + return p.Default() + } + return *p.val +} + +func (p *StringParam) Args() []string { + return []string{fmt.Sprintf("--%v", p.CLIName()), p.String()} +} + +func (p *StringParam) EnvVars() (string, string) { + return p.EnvName(), p.String() +} + +func (p *StringParam) Vars() (string, string) { + return p.Name(), p.String() +} + +type BoolParam struct { + paramCommon + val *bool +} + +func (p *BoolParam) New() Param { + return &BoolParam{p.paramCommon, nil} +} + +func (p *BoolParam) Vars() (string, string) { + return p.Name(), p.String() +} + +func (p *BoolParam) Set(s string) error { + v, err := strconv.ParseBool(s) + if err != nil { + return err + } + p.val = &v + return nil +} + +func (p *BoolParam) String() string { + if p.val == nil { + return "false" + } + return fmt.Sprintf("%v", *p.val) +} + +func (p *BoolParam) Args() []string { + return []string{fmt.Sprintf("--%v", p.CLIName()), p.String()} +} + +func (p *BoolParam) EnvVars() (string, string) { + return p.EnvName(), p.String() +} + +type IntParam struct { + paramCommon + val *int64 +} + +func (p *IntParam) Set(s string) error { + v, err := strconv.ParseInt(s, 0, 64) + if err != nil { + return err + } + p.val = &v + return nil +} + +func (p *IntParam) String() string { + if p.val == nil { + return p.Default() + } + return fmt.Sprintf("%v", *p.val) +} + +func (p *IntParam) New() Param { + return &IntParam{p.paramCommon, nil} +} + +func (p *IntParam) Args() []string { + return []string{fmt.Sprintf("--%v", p.CLIName()), p.String()} +} + +func (p *IntParam) EnvVars() (string, string) { + return p.EnvName(), p.String() +} + +func (p *IntParam) Vars() (string, string) { + return p.Name(), p.String() +} + +type ListParam struct { + paramCommon + el Param + values []Param +} + +func (p *ListParam) CLIName() string { + return p.el.CLIName() +} + +func (p *ListParam) EnvName() string { + return p.el.EnvName() +} + +func (p *ListParam) Set(s string) error { + // this is to support setting from environment variables + values := cstrings.Split(',', '\\', s) + for _, v := range values { + el := p.el.New() + if err := el.Set(v); err != nil { + return err + } + p.values = append(p.values, el) + } + return nil +} + +func (p *ListParam) New() Param { + return &ListParam{p.paramCommon, p.el, nil} +} + +func (p *ListParam) String() string { + if len(p.values) == 0 { + return p.Default() + } + out := make([]string, len(p.values)) + for i, v := range p.values { + out[i] = v.String() + } + return fmt.Sprintf("[%v]", strings.Join(out, ",")) +} + +func (p *ListParam) Args() []string { + if len(p.values) == 0 { + return []string{} + } + out := make([]string, 0, len(p.values)) + for _, v := range p.values { + out = append(out, v.Args()...) + } + return out +} + +func (p *ListParam) EnvVars() (string, string) { + if len(p.values) == 0 { + return p.EnvName(), p.Default() + } + out := make([]string, len(p.values)) + for i, v := range p.values { + _, out[i] = v.EnvVars() + } + return p.el.EnvName(), strings.Join(out, ",") +} + +func (p *ListParam) Vars() (string, string) { + if len(p.values) == 0 { + return p.Name(), p.Default() + } + out := make([]string, len(p.values)) + for i, v := range p.values { + _, out[i] = v.EnvVars() + } + return p.Name(), strings.Join(out, ",") +} + +type KVParam struct { + paramCommon + separator string + keys []Param + values []Param +} + +func (p *KVParam) sep() string { + if p.separator == "" { + return ":" + } + return "" +} + +func (p *KVParam) Set(s string) error { + sep := p.sep() + + parts := strings.Split(s, sep) + if len(parts) != len(p.keys) { + return trace.Errorf( + "expected elements separated by '%v', got '%v'", sep, s) + } + values := make([]Param, len(p.keys)) + for i, pt := range parts { + el := p.keys[i].New() + if err := el.Set(pt); err != nil { + return err + } + values[i] = el + } + + p.values = values + return nil +} + +func (p *KVParam) String() string { + if len(p.values) == 0 { + return p.Default() + } + out := make([]string, len(p.values)) + for i, v := range p.values { + out[i] = v.String() + } + return fmt.Sprintf("{%v}", strings.Join(out, p.sep())) +} + +func (p *KVParam) New() Param { + keys := make([]Param, len(p.keys)) + for i, k := range p.keys { + keys[i] = k.New() + } + return &KVParam{p.paramCommon, p.separator, keys, nil} +} + +func (p *KVParam) Args() []string { + if len(p.values) == 0 { + return []string{} + } + vals := make([]string, len(p.values)) + for i, v := range p.values { + vals[i] = v.String() + } + return []string{ + fmt.Sprintf("--%v", p.CLIName()), strings.Join(vals, p.sep())} +} + +func (p *KVParam) EnvVars() (string, string) { + if len(p.values) == 0 { + return p.EnvName(), p.Default() + } + vals := make([]string, len(p.values)) + for i, v := range p.values { + vals[i] = v.String() + } + return p.EnvName(), strings.Join(vals, p.sep()) +} + +func (p *KVParam) Vars() (string, string) { + if len(p.values) == 0 { + return p.Name(), p.Default() + } + vals := make([]string, len(p.values)) + for i, v := range p.values { + vals[i] = v.String() + } + return p.Name(), strings.Join(vals, p.sep()) +} + +type EnumParam struct { + paramCommon + values []string + value *string +} + +func (p *EnumParam) Set(s string) error { + found := false + for _, v := range p.values { + if s == v { + found = true + } + } + if !found { + return trace.Errorf( + "value '%v' is not one of the allowed '%v'", + s, strings.Join(p.values, ","), + ) + } + p.value = &s + return nil +} + +func (p *EnumParam) String() string { + if p.value == nil { + return p.Default() + } + return *p.value +} + +func (p *EnumParam) New() Param { + return &EnumParam{p.paramCommon, p.values, nil} +} + +func (p *EnumParam) Args() []string { + if p.value == nil { + return []string{} + } + return []string{fmt.Sprintf("--%v", p.CLIName()), *p.value} +} + +func (p *EnumParam) EnvVars() (string, string) { + return p.EnvName(), p.String() +} + +func (p *EnumParam) Vars() (string, string) { + return p.Name(), p.String() +} diff --git a/Godeps/_workspace/src/github.com/gravitational/configure/schema/schema_test.go b/Godeps/_workspace/src/github.com/gravitational/configure/schema/schema_test.go new file mode 100644 index 00000000000..332080395fd --- /dev/null +++ b/Godeps/_workspace/src/github.com/gravitational/configure/schema/schema_test.go @@ -0,0 +1,487 @@ +package schema + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/kr/pretty" + "github.com/kylelemons/godebug/diff" + + . "github.com/gravitational/teleport/Godeps/_workspace/src/gopkg.in/check.v1" +) + +func TestConfig(t *testing.T) { TestingT(t) } + +type ConfigSuite struct { +} + +var _ = Suite(&ConfigSuite{}) + +func (s *ConfigSuite) TestParseTypes(c *C) { + tcs := []struct { + name string + cfg string + expect *Config + }{ + { + name: "string param", + cfg: `{ + "params": [ + { + "name": "string1", + "type": "String" + } + ] + }`, + expect: &Config{ + Params: []Param{ + &StringParam{paramCommon{name: "string1"}, nil}, + }, + }, + }, + { + name: "bool param", + cfg: `{ + "params": [ + { + "name": "bool1", + "type": "Bool" + } + ] + }`, + expect: &Config{ + Params: []Param{ + &BoolParam{paramCommon{name: "bool1"}, nil}, + }, + }, + }, + { + name: "enum param", + cfg: `{ + "params": [ + { + "name": "enum1", + "type": "Enum", + "spec": { + "values": ["a", "b"] + } + } + ] + }`, + expect: &Config{ + Params: []Param{ + &EnumParam{paramCommon{name: "enum1"}, []string{"a", "b"}, nil}, + }, + }, + }, + { + name: "key val param", + cfg: `{ + "params": [ + { + "name": "kv1", + "type": "KeyVal", + "default": "path1:hello", + "required": true, + "spec": { + "separator": ":", + "keys": [ + {"type": "Path", "name": "path1"}, + {"type": "Path", "name": "path2"} + ] + } + } + ] + }`, + expect: &Config{ + Params: []Param{ + &KVParam{ + paramCommon{name: "kv1", req: true, def: "path1:hello"}, + ":", + []Param{ + &PathParam{paramCommon{name: "path1"}, nil}, + &PathParam{paramCommon{name: "path2"}, nil}, + }, + nil, + }, + }, + }, + }, + { + name: "list key val param", + cfg: `{ + "params": [ + { + "name": "mounts", + "type": "List", + "spec": { + "name": "volume", + "type": "KeyVal", + "spec": { + "separator": ":", + "keys": [ + {"type": "Path", "name": "path1"}, + {"type": "Path", "name": "path2"} + ] + } + } + } + ] + }`, + expect: &Config{ + Params: []Param{ + &ListParam{ + paramCommon{name: "mounts"}, + &KVParam{ + paramCommon{name: "volume"}, + ":", + []Param{ + &PathParam{paramCommon{name: "path1"}, nil}, + &PathParam{paramCommon{name: "path2"}, nil}, + }, + nil, + }, + nil, + }, + }, + }, + }, + } + for i, tc := range tcs { + comment := Commentf("test #%d (%v) cfg=%v, param=%v", i+1, tc.name, tc.cfg) + cfg, err := ParseJSON(strings.NewReader(tc.cfg)) + c.Assert(err, IsNil, comment) + c.Assert(len(cfg.Params), Equals, len(tc.expect.Params)) + for i, _ := range cfg.Params { + c.Assert(cfg.Params[i], DeepEquals, tc.expect.Params[i], comment) + } + } +} + +func (s *ConfigSuite) TestArgs(c *C) { + tcs := []struct { + name string + cfg string + expect *Config + expectErr bool + args []string + vars map[string]string + }{ + { + name: "string param", + cfg: `{ + "params": [ + { + "name": "string1", + "type": "String" + } + ] + }`, + expect: &Config{ + Params: []Param{ + &StringParam{paramCommon{name: "string1"}, str("val1")}, + }, + }, + args: []string{"--string1", "val1"}, + vars: map[string]string{"STRING1": "val1"}, + }, + { + name: "check defaults", + cfg: `{ + "params": [ + { + "name": "string1", + "type": "String", + "default": "default value" + } + ] + }`, + expect: &Config{ + Params: []Param{ + &StringParam{ + paramCommon{name: "string1", def: "default value"}, + str("default value")}, + }, + }, + args: []string{}, + vars: map[string]string{"STRING1": "default value"}, + }, + { + name: "check missing required param", + cfg: `{ + "params": [ + { + "name": "string1", + "type": "String", + "required": true + } + ] + }`, + expectErr: true, + args: []string{}, + }, + { + name: "check key values", + cfg: `{ + "params": [ + { + "name": "volume", + "env": "PREFIX_VOLUME", + "type": "KeyVal", + "spec": { + "keys": [ + {"name": "src", "type":"Path"}, + {"name": "dst", "type":"Path"} + ] + } + } + ] + }`, + args: []string{"--volume", "/tmp/hello:/var/hello"}, + vars: map[string]string{"PREFIX_VOLUME": "/tmp/hello:/var/hello"}, + expect: &Config{ + Params: []Param{ + &KVParam{ + paramCommon{name: "volume", env: "PREFIX_VOLUME"}, + "", + []Param{ + &PathParam{paramCommon{name: "src"}, nil}, + &PathParam{paramCommon{name: "dst"}, nil}, + }, + []Param{ + &PathParam{paramCommon{name: "src"}, str("/tmp/hello")}, + &PathParam{paramCommon{name: "dst"}, str("/var/hello")}, + }, + }, + }, + }, + }, + { + name: "list of key values", + cfg: `{ + "params": [ + { + "type": "List", + "name": "mounts", + "spec": { + "name": "volume", + "type": "KeyVal", + "spec": { + "keys": [ + {"name": "src", "type":"Path"}, + {"name": "dst", "type":"Path"} + ] + } + } + } + ] + }`, + args: []string{"--volume", "/tmp/hello:/var/hello"}, + vars: map[string]string{"VOLUME": "/tmp/hello:/var/hello"}, + expect: &Config{ + Params: []Param{ + &ListParam{ + paramCommon{name: "mounts"}, + &KVParam{ + paramCommon{name: "volume"}, + "", + []Param{ + &PathParam{paramCommon{name: "src"}, nil}, + &PathParam{paramCommon{name: "dst"}, nil}, + }, + nil, + }, + []Param{ + &KVParam{ + paramCommon{name: "volume"}, + "", + []Param{ + &PathParam{paramCommon{name: "src"}, nil}, + &PathParam{paramCommon{name: "dst"}, nil}, + }, + []Param{ + &PathParam{paramCommon{name: "src"}, str("/tmp/hello")}, + &PathParam{paramCommon{name: "dst"}, str("/var/hello")}, + }, + }, + }, + }, + }, + }, + }, + } + for i, tc := range tcs { + comment := Commentf( + "test #%d (%v) cfg=%v, args=%v", i+1, tc.name, tc.cfg, tc.args) + cfg, err := ParseJSON(strings.NewReader(tc.cfg)) + c.Assert(err, IsNil, comment) + + if tc.expectErr { + c.Assert(cfg.ParseArgs(tc.args), NotNil) + continue + } + + // make sure all the values have been parsed + c.Assert(cfg.ParseArgs(tc.args), IsNil) + c.Assert(len(cfg.Params), Equals, len(tc.expect.Params)) + for i, _ := range cfg.Params { + comment := Commentf( + "test #%d (%v) cfg=%v, args=%v\n%v", + i+1, tc.name, tc.cfg, tc.args, + diff.Diff( + fmt.Sprintf("%# v", pretty.Formatter(cfg.Params[i])), + fmt.Sprintf("%# v", pretty.Formatter(tc.expect.Params[i]))), + ) + c.Assert(cfg.Params[i], DeepEquals, tc.expect.Params[i], comment) + } + + // make sure args are equivalent to the passed arguments + if len(tc.args) != 0 { + args := cfg.Args() + c.Assert(args, DeepEquals, tc.args, comment) + } + + // make sure vars are what we expect them to be + if len(tc.vars) != 0 { + c.Assert(cfg.EnvVars(), DeepEquals, tc.vars, comment) + } + } +} + +func (s *ConfigSuite) TestEnvVars(c *C) { + tcs := []struct { + name string + cfg string + expect map[string]string + }{ + { + name: "string param", + cfg: `{ + "params": [ + { + "env": "ENV_STRING1", + "name": "string1", + "type": "String" + } + ] + }`, + expect: map[string]string{"ENV_STRING1": "val1"}, + }, + { + name: "list of key values", + cfg: `{ + "params": [ + { + "type": "List", + "name": "mounts", + "spec": { + "name": "volume", + "env": "PREFIX_VOLUME", + "type": "KeyVal", + "spec": { + "keys": [ + {"name": "src", "type":"Path"}, + {"name": "dst", "type":"Path"} + ] + } + } + } + ] + }`, + expect: map[string]string{ + "PREFIX_VOLUME": "/tmp/hello:/var/hello,/tmp/hello1:/var/hello2", + }, + }, + } + for i, tc := range tcs { + comment := Commentf( + "test #%d (%v) cfg=%v", i+1, tc.name, tc.cfg) + cfg, err := ParseJSON(strings.NewReader(tc.cfg)) + c.Assert(err, IsNil, comment) + + os.Clearenv() + for k, v := range tc.expect { + os.Setenv(k, v) + } + c.Assert(cfg.ParseEnv(), IsNil) + c.Assert(cfg.EnvVars(), DeepEquals, tc.expect, comment) + } +} + +func (s *ConfigSuite) TestParseVars(c *C) { + tcs := []struct { + name string + cfg string + expect map[string]string + }{ + { + name: "string param", + cfg: `{ + "params": [ + { + "env": "ENV_STRING1", + "name": "string1", + "type": "String" + } + ] + }`, + expect: map[string]string{"string1": "val1"}, + }, + { + name: "int param default", + cfg: `{ + "params": [ + { + "name": "int1", + "type": "Int", + "default": "-1" + } + ] + }`, + expect: map[string]string{"int1": "-1"}, + }, + { + name: "list of key values", + cfg: `{ + "params": [ + { + "type": "List", + "name": "mounts", + "spec": { + "name": "volume", + "type": "KeyVal", + "spec": { + "keys": [ + {"name": "src", "type":"Path"}, + {"name": "dst", "type":"Path"} + ] + } + } + } + ] + }`, + expect: map[string]string{ + "mounts": "/tmp/hello:/var/hello,/tmp/hello1:/var/hello2", + }, + }, + } + for i, tc := range tcs { + comment := Commentf( + "test #%d (%v) cfg=%v", i+1, tc.name, tc.cfg) + cfg, err := ParseJSON(strings.NewReader(tc.cfg)) + c.Assert(err, IsNil, comment) + + vars := make(map[string]string) + for k, v := range tc.expect { + vars[k] = v + } + c.Assert(cfg.ParseVars(vars), IsNil) + c.Assert(cfg.Vars(), DeepEquals, tc.expect, comment) + } +} + +func str(val string) *string { + return &val +} diff --git a/lib/service/cfg.go b/lib/service/cfg.go index f3fab16fd1e..a0c3458e02b 100644 --- a/lib/service/cfg.go +++ b/lib/service/cfg.go @@ -10,6 +10,7 @@ import ( "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/configure" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/configure/cstrings" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/trace" ) @@ -169,7 +170,7 @@ type ReverseTunnelConfig struct { type NetAddrSlice []utils.NetAddr func (s *NetAddrSlice) Set(val string) error { - values := configure.SplitComma(val) + values := cstrings.SplitComma(val) out := make([]utils.NetAddr, len(values)) for i, v := range values { a, err := utils.ParseAddr(v) @@ -189,7 +190,7 @@ func (kv *KeyVal) Set(v string) error { if len(*kv) == 0 { *kv = make(map[string]string) } - for _, i := range configure.SplitComma(v) { + for _, i := range cstrings.SplitComma(v) { vals := strings.SplitN(i, ":", 2) if len(vals) != 2 { return trace.Errorf("extra options should be defined like KEY:VAL") diff --git a/lib/service/tpl.go b/lib/service/tpl.go index eb6ca06d7f1..88f33541a06 100644 --- a/lib/service/tpl.go +++ b/lib/service/tpl.go @@ -8,7 +8,7 @@ import ( "strings" "text/template" - "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/configure" + "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/configure/cstrings" "github.com/gravitational/teleport/Godeps/_workspace/src/github.com/gravitational/trace" ) @@ -60,7 +60,7 @@ func (c *ctx) Env(key string) (string, error) { if !ok { return "", trace.Errorf("environment variable '%v' is not set", key) } - values := configure.SplitComma(v) + values := cstrings.SplitComma(v) out := make([]string, len(values)) for i, p := range values { out[i] = quoteYAML(p)