Skip to content

Commit

Permalink
Move Kibana configuration in a Secret (#977)
Browse files Browse the repository at this point in the history
* Migrate all env vars in the config settings file
* Reconcile a new config secret using a static kibana.yml config settings file (named <name>-kb-config)
* Update the deployment spec with the new config volume and volumeMount
* Reuse Elasticsearch settings CanonicalConfig bits (move in a common package)
  • Loading branch information
thbkrkr authored Jun 6, 2019
1 parent f4719ab commit 51214a2
Show file tree
Hide file tree
Showing 35 changed files with 931 additions and 701 deletions.
290 changes: 290 additions & 0 deletions operators/pkg/controller/common/settings/canonical_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package settings

import (
"fmt"
"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.v2"
)

// 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(".")}

// NewCanonicalConfig creates a new empty config.
func NewCanonicalConfig() *CanonicalConfig {
return fromConfig(ucfg.New())
}

// NewCanonicalConfigFrom creates a new config from the API type.
func NewCanonicalConfigFrom(data untypedDict) (*CanonicalConfig, error) {
config, err := ucfg.NewFrom(data, 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.
// Convenience constructor, will panic in the unlikely event of errors.
func MustNewSingleValue(k string, v string) *CanonicalConfig {
cfg := fromConfig(ucfg.New())
err := cfg.asUCfg().SetString(k, -1, v, Options...)
if err != nil {
panic(err)
}
return cfg
}

// 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
}

// 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
}

// 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
}

// HasPrefixes returns all keys in c that have one of the given prefix keys.
// Keys are expected in dotted form.
func (c *CanonicalConfig) HasPrefixes(keys []string) []string {
var has []string
flatKeys := c.asUCfg().FlattenedKeys(Options...)
for _, s := range keys {
for _, k := range flatKeys {
if strings.HasPrefix(k, s) {
has = append(has, s)
}
}
}
return has
}

// 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
err := c.asUCfg().Unpack(&out)
if 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
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, fmt.Errorf("map assertation 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, fmt.Errorf("slice assertation 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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/elastic/cloud-on-k8s/operators/pkg/apis/elasticsearch/v1alpha1"
"github.com/elastic/cloud-on-k8s/operators/pkg/controller/elasticsearch/pod"
"github.com/elastic/cloud-on-k8s/operators/pkg/controller/elasticsearch/settings"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -26,7 +27,7 @@ func namedPod(name string) pod.PodWithConfig {
Name: name,
},
},
Config: nil,
Config: settings.CanonicalConfig{},
}
}

Expand All @@ -38,7 +39,7 @@ func namedPodWithCreationTimestamp(name string, creationTimestamp time.Time) pod
CreationTimestamp: metav1.Time{Time: creationTimestamp},
},
},
Config: nil,
Config: settings.CanonicalConfig{},
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import (
"github.com/elastic/cloud-on-k8s/operators/pkg/controller/elasticsearch/settings"
)

func compareConfigs(actual *settings.CanonicalConfig, expected *settings.CanonicalConfig) Comparison {
func compareConfigs(actual settings.CanonicalConfig, expected settings.CanonicalConfig) Comparison {
// check for settings in actual that do not match expected
diff := actual.Diff(expected, toIgnore)
diff := actual.Diff(expected.CanonicalConfig, toIgnore)
if len(diff) == 0 {
return ComparisonMatch
}
Expand Down
Loading

0 comments on commit 51214a2

Please sign in to comment.