config/config.go (279 lines of code) (raw):

// Licensed to Elasticsearch B.V. under one or more contributor // license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright // ownership. Elasticsearch B.V. licenses this file to you under // the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package config import ( "encoding/json" "errors" "fmt" "strings" "sync" "github.com/elastic/elastic-agent-libs/str" ucfg "github.com/elastic/go-ucfg" "github.com/elastic/go-ucfg/yaml" ) // C object to store hierarchical configurations into. // See https://godoc.org/github.com/elastic/go-ucfg#Config type C ucfg.Config // Namespace stores at most one configuration section by name and sub-section. type Namespace struct { name string config *C } const mask = "xxxxx" var ( configOptsMu sync.RWMutex configOpts = []ucfg.Option{ ucfg.PathSep("."), ucfg.ResolveEnv, ucfg.VarExp, } maskList = str.MakeSet( "password", "passphrase", "key_passphrase", "pass", "proxy_url", "url", "urls", "host", "hosts", "authorization", "proxy-authorization", ) ) func getGlobalConfigOpts() []ucfg.Option { configOptsMu.RLock() defer configOptsMu.RUnlock() return configOpts } func setGlobalConfigOpts(opts []ucfg.Option) { configOptsMu.Lock() defer configOptsMu.Unlock() configOpts = opts } func NewConfig() *C { return fromConfig(ucfg.New()) } // NewConfigFrom creates a new C object from the given input. // From can be any kind of structured data (struct, map, array, slice). // // If from is a string, the contents is treated like raw YAML input. The string // will be parsed and a structure config object is build from the parsed // result. func NewConfigFrom(from interface{}) (*C, error) { if str, ok := from.(string); ok { c, err := yaml.NewConfig([]byte(str), getGlobalConfigOpts()...) return fromConfig(c), err } c, err := ucfg.NewFrom(from, getGlobalConfigOpts()...) return fromConfig(c), err } // MustNewConfigFrom creates a new C object from the given input. // From can be any kind of structured data (struct, map, array, slice). // // If from is a string, the contents is treated like raw YAML input. The string // will be parsed and a structure config object is build from the parsed // result. // // MustNewConfigFrom panics if an error occurs. func MustNewConfigFrom(from interface{}) *C { cfg, err := NewConfigFrom(from) if err != nil { panic(err) } return cfg } // MergeConfigs merges the configs together. If there are // different values for the same key, the last one always overwrites // the previous values. func MergeConfigs(cfgs ...*C) (*C, error) { config := NewConfig() for _, c := range cfgs { if err := config.Merge(c); err != nil { return nil, err } } return config, nil } // MergeConfigs merges the configs together based on the provided opts. // If there are different values for the same key, the last one always overwrites // the previous values. func MergeConfigsWithOptions(cfgs []*C, options ...ucfg.Option) (*C, error) { config := NewConfig() for _, c := range cfgs { if err := config.MergeWithOpts(c, options...); err != nil { return nil, err } } return config, nil } // NewConfigWithYAML reads a YAML configuration. func NewConfigWithYAML(in []byte, source string) (*C, error) { opts := append( []ucfg.Option{ ucfg.MetaData(ucfg.Meta{Source: source}), }, getGlobalConfigOpts()..., ) c, err := yaml.NewConfig(in, opts...) return fromConfig(c), err } // OverwriteConfigOpts allow to change the globally set config option func OverwriteConfigOpts(options []ucfg.Option) { setGlobalConfigOpts(options) } // Merge merges the parameter into the C object. func (c *C) Merge(from interface{}) error { return c.access().Merge(from, getGlobalConfigOpts()...) } // Merge merges the parameter into the C object based on the provided options. func (c *C) MergeWithOpts(from interface{}, opts ...ucfg.Option) error { o := getGlobalConfigOpts() if opts != nil { o = append(o, opts...) } return c.access().Merge(from, o...) } func (c *C) Unpack(to interface{}) error { return c.access().Unpack(to, getGlobalConfigOpts()...) } func (c *C) Path() string { return c.access().Path(".") } func (c *C) PathOf(field string) string { return c.access().PathOf(field, ".") } func (c *C) Remove(name string, idx int) (bool, error) { return c.access().Remove(name, idx, getGlobalConfigOpts()...) } func (c *C) Has(name string, idx int) (bool, error) { return c.access().Has(name, idx, getGlobalConfigOpts()...) } func (c *C) HasField(name string) bool { return c.access().HasField(name) } func (c *C) CountField(name string) (int, error) { return c.access().CountField(name) } func (c *C) Bool(name string, idx int) (bool, error) { return c.access().Bool(name, idx, getGlobalConfigOpts()...) } func (c *C) String(name string, idx int) (string, error) { return c.access().String(name, idx, getGlobalConfigOpts()...) } func (c *C) Int(name string, idx int) (int64, error) { return c.access().Int(name, idx, getGlobalConfigOpts()...) } func (c *C) Float(name string, idx int) (float64, error) { return c.access().Float(name, idx, getGlobalConfigOpts()...) } func (c *C) Child(name string, idx int) (*C, error) { sub, err := c.access().Child(name, idx, getGlobalConfigOpts()...) return fromConfig(sub), err } func (c *C) SetBool(name string, idx int, value bool) error { return c.access().SetBool(name, idx, value, getGlobalConfigOpts()...) } func (c *C) SetInt(name string, idx int, value int64) error { return c.access().SetInt(name, idx, value, getGlobalConfigOpts()...) } func (c *C) SetFloat(name string, idx int, value float64) error { return c.access().SetFloat(name, idx, value, getGlobalConfigOpts()...) } func (c *C) SetString(name string, idx int, value string) error { return c.access().SetString(name, idx, value, getGlobalConfigOpts()...) } func (c *C) SetChild(name string, idx int, value *C) error { return c.access().SetChild(name, idx, value.access(), getGlobalConfigOpts()...) } func (c *C) IsDict() bool { return c.access().IsDict() } func (c *C) IsArray() bool { return c.access().IsArray() } // FlattenedKeys return a sorted flattened views of the set keys in the configuration. func (c *C) FlattenedKeys() []string { return c.access().FlattenedKeys(getGlobalConfigOpts()...) } // Enabled return the configured enabled value or true by default. func (c *C) Enabled() bool { testEnabled := struct { Enabled bool `config:"enabled"` }{true} if c == nil { return false } if err := c.Unpack(&testEnabled); err != nil { // if unpacking fails, expect 'enabled' being set to default value return true } return testEnabled.Enabled } func fromConfig(in *ucfg.Config) *C { return (*C)(in) } func (c *C) access() *ucfg.Config { return (*ucfg.Config)(c) } // GetFields returns the list of fields in the configuration. func (c *C) GetFields() []string { return c.access().GetFields() } // Unpack unpacks a configuration with at most one sub object. An sub object is // ignored if it is disabled by setting `enabled: false`. If the configuration // passed contains multiple active sub objects, Unpack will return an error. func (ns *Namespace) Unpack(cfg *C) error { fields := cfg.GetFields() if len(fields) == 0 { return nil } var ( err error found bool ) for _, name := range fields { var sub *C sub, err = cfg.Child(name, -1) if err != nil { // element is no configuration object -> continue so a namespace // Config unpacked as a namespace can have other configuration // values as well continue } if !sub.Enabled() { continue } if ns.name != "" { return errors.New("more than one namespace configured") } ns.name = name ns.config = sub found = true } if !found { return err } return nil } // Name returns the configuration sections it's name if a section has been set. func (ns *Namespace) Name() string { return ns.name } // Config return the sub-configuration section if a section has been set. func (ns *Namespace) Config() *C { return ns.config } // IsSet returns true if a sub-configuration section has been set. func (ns *Namespace) IsSet() bool { return ns.config != nil } // DebugString prints a human readable representation of the underlying config using // JSON formatting. func DebugString(c *C, filterPrivate bool) string { var bufs []string if c.IsDict() { var content map[string]interface{} if err := c.Unpack(&content); err != nil { return fmt.Sprintf("<config error> %v", err) } if filterPrivate { ApplyLoggingMask(content) } j, _ := json.MarshalIndent(content, "", " ") bufs = append(bufs, string(j)) } if c.IsArray() { var content []interface{} if err := c.Unpack(&content); err != nil { return fmt.Sprintf("<config error> %v", err) } if filterPrivate { ApplyLoggingMask(content) } j, _ := json.MarshalIndent(content, "", " ") bufs = append(bufs, string(j)) } if len(bufs) == 0 { return "" } return strings.Join(bufs, "\n") } // ApplyLoggingMask redacts the values of keys that might // contain sensitive data (password, passphrase, etc.). func ApplyLoggingMask(c interface{}) { switch cfg := c.(type) { case map[string]interface{}: for k, v := range cfg { if maskList.Has(strings.ToLower(k)) { if arr, ok := v.([]interface{}); ok { for i := range arr { arr[i] = mask } } else { cfg[k] = mask } } else { ApplyLoggingMask(v) } } case []interface{}: for _, elem := range cfg { ApplyLoggingMask(elem) } } }