loader/config.go (178 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 loader import ( "fmt" "io" "os" "gopkg.in/yaml.v2" "github.com/elastic/go-ucfg" "github.com/elastic/go-ucfg/cfgutil" ) // options hold the specified options type options struct { skipKeys []string } // Option is an option type that modifies how loading configs work type Option func(*options) // VarSkipKeys prevents variable expansion for these keys. // // The provided keys only skip if the keys are top-level keys. func VarSkipKeys(keys ...string) Option { return func(opts *options) { opts.skipKeys = keys } } // DefaultOptions defaults options used to read the configuration var DefaultOptions = []interface{}{ ucfg.PathSep("."), ucfg.ResolveEnv, ucfg.VarExp, VarSkipKeys("inputs"), } // Config custom type over a ucfg.Config to add new methods on the object. type Config ucfg.Config // New creates a new empty config. func New() *Config { return newConfigFrom(ucfg.New()) } // NewConfigFrom takes a interface and read the configuration like it was YAML. func NewConfigFrom(from interface{}, opts ...interface{}) (*Config, error) { if len(opts) == 0 { opts = DefaultOptions } var ucfgOpts []ucfg.Option var localOpts []Option for _, o := range opts { switch ot := o.(type) { case ucfg.Option: ucfgOpts = append(ucfgOpts, ot) case Option: localOpts = append(localOpts, ot) default: return nil, fmt.Errorf("unknown option type %T", o) } } local := &options{} for _, o := range localOpts { o(local) } var data map[string]interface{} var err error if bytes, ok := from.([]byte); ok { err = yaml.Unmarshal(bytes, &data) if err != nil { return nil, err } } else if str, ok := from.(string); ok { err = yaml.Unmarshal([]byte(str), &data) if err != nil { return nil, err } } else if in, ok := from.(io.Reader); ok { if closer, ok := from.(io.Closer); ok { defer closer.Close() } fData, err := io.ReadAll(in) if err != nil { return nil, err } err = yaml.Unmarshal(fData, &data) if err != nil { return nil, err } } else if contents, ok := from.(map[string]interface{}); ok { data = contents } else { c, err := ucfg.NewFrom(from, ucfgOpts...) return newConfigFrom(c), err } skippedKeys := map[string]interface{}{} for _, skip := range local.skipKeys { val, ok := data[skip] if ok { skippedKeys[skip] = val delete(data, skip) } } cfg, err := ucfg.NewFrom(data, ucfgOpts...) if err != nil { return nil, err } if len(skippedKeys) > 0 { err = cfg.Merge(skippedKeys, ucfg.ResolveNOOP) // we modified incoming object // cleanup so skipped keys are not missing for k, v := range skippedKeys { data[k] = v } } return newConfigFrom(cfg), err } // MustNewConfigFrom try to create a configuration based on the type passed as arguments and panic // on failures. func MustNewConfigFrom(from interface{}) *Config { c, err := NewConfigFrom(from) if err != nil { panic(fmt.Sprintf("could not read configuration %+v", err)) } return c } func newConfigFrom(in *ucfg.Config) *Config { return (*Config)(in) } // Unpack unpacks a struct to Config. func (c *Config) Unpack(to interface{}, opts ...interface{}) error { ucfgOpts, err := getUcfgOptions(opts...) if err != nil { return err } return c.access().Unpack(to, ucfgOpts...) } func (c *Config) access() *ucfg.Config { return (*ucfg.Config)(c) } // Merge merges two configuration together. func (c *Config) Merge(from interface{}, opts ...interface{}) error { ucfgOpts, err := getUcfgOptions(opts...) if err != nil { return err } return c.access().Merge(from, ucfgOpts...) } // ToMapStr takes the config and transform it into a map[string]interface{} func (c *Config) ToMapStr() (map[string]interface{}, error) { var m map[string]interface{} if err := c.Unpack(&m); err != nil { return nil, err } return m, nil } // Enabled return the configured enabled value or true by default. func (c *Config) 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 } // LoadFile take a path and load the file and return a new configuration. func LoadFile(path string) (*Config, error) { fp, err := os.Open(path) if err != nil { return nil, err } return NewConfigFrom(fp) } // LoadFiles takes multiples files, load and merge all of them in a single one. func LoadFiles(paths ...string) (*Config, error) { merger := cfgutil.NewCollector(nil) for _, path := range paths { cfg, err := LoadFile(path) if err := merger.Add(cfg.access(), err); err != nil { return nil, err } } return newConfigFrom(merger.Config()), nil } func getUcfgOptions(opts ...interface{}) ([]ucfg.Option, error) { if len(opts) == 0 { opts = DefaultOptions } var ucfgOpts []ucfg.Option for _, o := range opts { switch ot := o.(type) { case ucfg.Option: ucfgOpts = append(ucfgOpts, ot) case Option: // ignored during unpack continue default: return nil, fmt.Errorf("unknown option type %T", o) } } return ucfgOpts, nil }