pkg/controller/common/settings/canonical_config.go (265 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package settings import ( "reflect" "sort" "strconv" "strings" ucfg "github.com/elastic/go-ucfg" udiff "github.com/elastic/go-ucfg/diff" uyaml "github.com/elastic/go-ucfg/yaml" "github.com/pkg/errors" yaml "gopkg.in/yaml.v3" ) // CanonicalConfig contains configuration for an Elastic resource ("elasticsearch.yml" or "kibana.yml"), // as a hierarchical key-value configuration. type CanonicalConfig ucfg.Config // Options are config options for the YAML file. Currently contains only support for dotted keys. var Options = []ucfg.Option{ucfg.PathSep("."), ucfg.AppendValues, ucfg.EscapePath()} // NewCanonicalConfig creates a new empty config. func NewCanonicalConfig() *CanonicalConfig { return fromConfig(ucfg.New()) } // NewCanonicalConfigFrom creates a new config from the API type after normalizing the data. func NewCanonicalConfigFrom(data untypedDict) (*CanonicalConfig, error) { // not great: round trip through yaml to normalize untyped dict before creating config // to avoid numeric differences in configs due to JSON marshalling/deep copies being restricted to float bytes, err := yaml.Marshal(data) if err != nil { return nil, err } var normalized untypedDict if err := yaml.Unmarshal(bytes, &normalized); err != nil { return nil, err } config, err := ucfg.NewFrom(normalized, Options...) if err != nil { return nil, err } return fromConfig(config), nil } // MustCanonicalConfig creates a new config and panics on errors. // Use for testing only. func MustCanonicalConfig(cfg interface{}) *CanonicalConfig { config, err := ucfg.NewFrom(cfg, Options...) if err != nil { panic(err) } return fromConfig(config) } // MustNewSingleValue creates a new config holding a single string value. // It is NewSingleValue but panics rather than returning errors, largely used for convenience in tests func MustNewSingleValue(k string, v string) *CanonicalConfig { cfg := NewCanonicalConfig() err := cfg.asUCfg().SetString(k, -1, v, Options...) if err != nil { panic(err) } return cfg } // NewSingleValue creates a new config holding a single string value. func NewSingleValue(k string, v string) (*CanonicalConfig, error) { cfg := fromConfig(ucfg.New()) err := cfg.asUCfg().SetString(k, -1, v, Options...) if err != nil { return nil, errors.WithStack(err) } return cfg, nil } // ParseConfig parses the given configuration content into a CanonicalConfig. // Expects content to be in YAML format. func ParseConfig(yml []byte) (*CanonicalConfig, error) { config, err := uyaml.NewConfig(yml, Options...) if err != nil { return nil, err } return fromConfig(config), nil } // MustParseConfig parses the given configuration content into a CanonicalConfig. // Expects content to be in YAML format. Panics on error. func MustParseConfig(yml []byte) *CanonicalConfig { config, err := uyaml.NewConfig(yml, Options...) if err != nil { panic(err) } return fromConfig(config) } // SetStrings sets key to string vals in c. An error is returned if key is invalid. func (c *CanonicalConfig) SetStrings(key string, vals ...string) error { if c == nil { return errors.New("config is nil") } switch len(vals) { case 0: return errors.New("Nothing to set") default: for i, v := range vals { err := c.asUCfg().SetString(key, i, v, Options...) if err != nil { return err } } } return nil } // String returns a string value for the given key func (c *CanonicalConfig) String(key string) (string, error) { return c.asUCfg().String(key, -1, Options...) } // Unpack returns a typed config given a struct pointer. func (c *CanonicalConfig) Unpack(cfg interface{}) error { if reflect.ValueOf(cfg).Kind() != reflect.Ptr { panic("Unpack expects a struct pointer as argument") } return c.asUCfg().Unpack(cfg, Options...) } // MergeWith merges the content of c and c2. // In case of conflict, c2 is taking precedence. func (c *CanonicalConfig) MergeWith(cfgs ...*CanonicalConfig) error { for _, c2 := range cfgs { if c2 == nil { continue } err := c.asUCfg().Merge(c2.asUCfg(), Options...) if err != nil { return err } } return nil } // HasKeys returns all keys in c that are also in keys func (c *CanonicalConfig) HasKeys(keys []string) []string { var has []string for _, s := range keys { hasKey, err := c.asUCfg().Has(s, 0, Options...) if err != nil || hasKey { has = append(has, s) } } return has } // HasChildConfig returns true if c has a child config object below key. func (c *CanonicalConfig) HasChildConfig(key string) bool { if c == nil { return false } // We are expecting two kinds of error here: // type mismatch: if key is pointing to a primitive value that means we don't have a child config // missing path: if the key does not exist in the config we also do not have a child config // There should be no other errors thrown by ucfg thus there is no error return type in this function. _, err := c.asUCfg().Child(key, -1, Options...) return err == nil } // Render returns the content of the configuration file, // with fields sorted alphabetically func (c *CanonicalConfig) Render() ([]byte, error) { if c == nil { return []byte{}, nil } var out untypedDict if err := c.asUCfg().Unpack(&out); err != nil { return []byte{}, err } return yaml.Marshal(out) } type untypedDict = map[string]interface{} // Diff returns the flattened keys where c and c2 differ. func (c *CanonicalConfig) Diff(c2 *CanonicalConfig, ignore []string) []string { var diff []string if c == c2 { return diff } if c == nil && c2 != nil { return c2.asUCfg().FlattenedKeys(Options...) } if c != nil && c2 == nil { return c.asUCfg().FlattenedKeys(Options...) } keyDiff := udiff.CompareConfigs(c.asUCfg(), c2.asUCfg(), Options...) diff = append(diff, keyDiff[udiff.Add]...) diff = append(diff, keyDiff[udiff.Remove]...) diff = removeIgnored(diff, ignore) if len(diff) > 0 { return diff } // at this point both configs should contain the same keys but may have different values var cUntyped untypedDict var c2Untyped untypedDict err := c.asUCfg().Unpack(&cUntyped, Options...) if err != nil { return []string{err.Error()} } err = c2.asUCfg().Unpack(&c2Untyped, Options...) if err != nil { return []string{err.Error()} } diff = diffMap(cUntyped, c2Untyped, "") return removeIgnored(diff, ignore) } func removeIgnored(diff, toIgnore []string) []string { var result []string //nolint:prealloc for _, d := range diff { if canIgnore(d, toIgnore) { continue } result = append(result, d) } sort.StringSlice(result).Sort() return result } func canIgnore(diff string, toIgnore []string) bool { for _, prefix := range toIgnore { if strings.HasPrefix(diff, prefix) { return true } } return false } func diffMap(c1, c2 untypedDict, key string) []string { // invariant: keys match // invariant: json-style map i.e no structs no pointers var diff []string for k, v := range c1 { newKey := k if len(key) != 0 { newKey = key + "." + k } v2 := c2[k] switch v.(type) { case untypedDict: l, r, err := asUntypedDict(v, v2) if err != nil { diff = append(diff, newKey) } diff = append(diff, diffMap(l, r, newKey)...) case []interface{}: l, r, err := asUntypedSlice(v, v2) if err != nil { diff = append(diff, newKey) } diff = append(diff, diffSlice(l, r, newKey)...) default: if v != v2 { diff = append(diff, newKey) } } } return diff } func diffSlice(s, s2 []interface{}, key string) []string { // invariant: keys match // invariant: s,s2 are json-style arrays/slices i.e no structs no pointers if len(s) != len(s2) { return []string{key} } var diff []string for i, v := range s { v2 := s2[i] newKey := key + "." + strconv.Itoa(i) switch v.(type) { case untypedDict: l, r, err := asUntypedDict(v, v2) if err != nil { diff = append(diff, newKey) } diff = append(diff, diffMap(l, r, newKey)...) case []interface{}: l, r, err := asUntypedSlice(v, v2) if err != nil { diff = append(diff, newKey) } diff = append(diff, diffSlice(l, r, newKey)...) default: if v != v2 { diff = append(diff, newKey) } } } return diff } func asUntypedDict(l, r interface{}) (untypedDict, untypedDict, error) { lhs, ok := l.(untypedDict) rhs, ok2 := r.(untypedDict) if !ok || !ok2 { return nil, nil, errors.Errorf("map assertion failed for l: %t r: %t", ok, ok2) } return lhs, rhs, nil } func asUntypedSlice(l, r interface{}) ([]interface{}, []interface{}, error) { lhs, ok := l.([]interface{}) rhs, ok2 := r.([]interface{}) if !ok || !ok2 { return nil, nil, errors.Errorf("slice assertion failed for l: %t r: %t", ok, ok2) } return lhs, rhs, nil } func (c *CanonicalConfig) asUCfg() *ucfg.Config { return (*ucfg.Config)(c) } func fromConfig(in *ucfg.Config) *CanonicalConfig { return (*CanonicalConfig)(in) }