Add KV store providers (dynamic configuration only)
Co-authored-by: Jean-Baptiste Doumenjou <jb.doumenjou@gmail.com>
This commit is contained in:
parent
028683666d
commit
9b9f4be6a4
61 changed files with 5825 additions and 70 deletions
75
pkg/config/kv/kv.go
Normal file
75
pkg/config/kv/kv.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package kv
|
||||
|
||||
import (
|
||||
"path"
|
||||
"reflect"
|
||||
|
||||
"github.com/abronan/valkeyrie/store"
|
||||
"github.com/containous/traefik/v2/pkg/config/parser"
|
||||
)
|
||||
|
||||
// Decode decodes the given KV pairs into the given element.
|
||||
// The operation goes through three stages roughly summarized as:
|
||||
// KV pairs -> tree of untyped nodes
|
||||
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
|
||||
// "typed" nodes -> typed element
|
||||
func Decode(pairs []*store.KVPair, element interface{}, rootName string) error {
|
||||
if element == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
filters := getRootFieldNames(rootName, element)
|
||||
|
||||
node, err := DecodeToNode(pairs, rootName, filters...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: false}
|
||||
err = parser.AddMetadata(element, node, metaOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return parser.Fill(element, node, parser.FillerOpts{AllowSliceAsStruct: false})
|
||||
}
|
||||
|
||||
func getRootFieldNames(rootName string, element interface{}) []string {
|
||||
if element == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
rootType := reflect.TypeOf(element)
|
||||
|
||||
return getFieldNames(rootName, rootType)
|
||||
}
|
||||
|
||||
func getFieldNames(rootName string, rootType reflect.Type) []string {
|
||||
var names []string
|
||||
|
||||
if rootType.Kind() == reflect.Ptr {
|
||||
rootType = rootType.Elem()
|
||||
}
|
||||
|
||||
if rootType.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < rootType.NumField(); i++ {
|
||||
field := rootType.Field(i)
|
||||
|
||||
if !parser.IsExported(field) {
|
||||
continue
|
||||
}
|
||||
|
||||
if field.Anonymous &&
|
||||
(field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct || field.Type.Kind() == reflect.Struct) {
|
||||
names = append(names, getFieldNames(rootName, field.Type)...)
|
||||
continue
|
||||
}
|
||||
|
||||
names = append(names, path.Join(rootName, field.Name))
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
128
pkg/config/kv/kv_node.go
Normal file
128
pkg/config/kv/kv_node.go
Normal file
|
@ -0,0 +1,128 @@
|
|||
package kv
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/abronan/valkeyrie/store"
|
||||
"github.com/containous/traefik/v2/pkg/config/parser"
|
||||
)
|
||||
|
||||
// DecodeToNode converts the labels to a tree of nodes.
|
||||
// If any filters are present, labels which do not match the filters are skipped.
|
||||
func DecodeToNode(pairs []*store.KVPair, rootName string, filters ...string) (*parser.Node, error) {
|
||||
sortedPairs := filterPairs(pairs, filters)
|
||||
|
||||
exp := regexp.MustCompile(`^\d+$`)
|
||||
|
||||
var node *parser.Node
|
||||
|
||||
for i, pair := range sortedPairs {
|
||||
split := strings.FieldsFunc(pair.Key, func(c rune) bool { return c == '/' })
|
||||
|
||||
if split[0] != rootName {
|
||||
return nil, fmt.Errorf("invalid label root %s", split[0])
|
||||
}
|
||||
|
||||
var parts []string
|
||||
for _, fragment := range split {
|
||||
if exp.MatchString(fragment) {
|
||||
parts = append(parts, "["+fragment+"]")
|
||||
} else {
|
||||
parts = append(parts, fragment)
|
||||
}
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
node = &parser.Node{}
|
||||
}
|
||||
decodeToNode(node, parts, string(pair.Value))
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
func decodeToNode(root *parser.Node, path []string, value string) {
|
||||
if len(root.Name) == 0 {
|
||||
root.Name = path[0]
|
||||
}
|
||||
|
||||
// it's a leaf or not -> children
|
||||
if len(path) > 1 {
|
||||
if n := containsNode(root.Children, path[1]); n != nil {
|
||||
// the child already exists
|
||||
decodeToNode(n, path[1:], value)
|
||||
} else {
|
||||
// new child
|
||||
child := &parser.Node{Name: path[1]}
|
||||
decodeToNode(child, path[1:], value)
|
||||
root.Children = append(root.Children, child)
|
||||
}
|
||||
} else {
|
||||
root.Value = value
|
||||
}
|
||||
}
|
||||
|
||||
func containsNode(nodes []*parser.Node, name string) *parser.Node {
|
||||
for _, n := range nodes {
|
||||
if strings.EqualFold(name, n.Name) {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func filterPairs(pairs []*store.KVPair, filters []string) []*store.KVPair {
|
||||
exp := regexp.MustCompile(`^(.+)/\d+$`)
|
||||
|
||||
sort.Slice(pairs, func(i, j int) bool {
|
||||
return pairs[i].Key < pairs[j].Key
|
||||
})
|
||||
|
||||
var simplePairs = map[string]*store.KVPair{}
|
||||
var slicePairs = map[string][]string{}
|
||||
|
||||
for _, pair := range pairs {
|
||||
if len(filters) == 0 {
|
||||
// Slice of simple type
|
||||
if exp.MatchString(pair.Key) {
|
||||
sanitizedKey := exp.FindStringSubmatch(pair.Key)[1]
|
||||
slicePairs[sanitizedKey] = append(slicePairs[sanitizedKey], string(pair.Value))
|
||||
} else {
|
||||
simplePairs[pair.Key] = pair
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for _, filter := range filters {
|
||||
if len(pair.Key) >= len(filter) && strings.EqualFold(pair.Key[:len(filter)], filter) {
|
||||
// Slice of simple type
|
||||
if exp.MatchString(pair.Key) {
|
||||
sanitizedKey := exp.FindStringSubmatch(pair.Key)[1]
|
||||
slicePairs[sanitizedKey] = append(slicePairs[sanitizedKey], string(pair.Value))
|
||||
} else {
|
||||
simplePairs[pair.Key] = pair
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sortedPairs []*store.KVPair
|
||||
for k, v := range slicePairs {
|
||||
delete(simplePairs, k)
|
||||
sortedPairs = append(sortedPairs, &store.KVPair{Key: k, Value: []byte(strings.Join(v, ","))})
|
||||
}
|
||||
|
||||
for _, v := range simplePairs {
|
||||
sortedPairs = append(sortedPairs, v)
|
||||
}
|
||||
|
||||
sort.Slice(sortedPairs, func(i, j int) bool {
|
||||
return sortedPairs[i].Key < sortedPairs[j].Key
|
||||
})
|
||||
|
||||
return sortedPairs
|
||||
}
|
274
pkg/config/kv/kv_node_test.go
Normal file
274
pkg/config/kv/kv_node_test.go
Normal file
|
@ -0,0 +1,274 @@
|
|||
package kv
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/abronan/valkeyrie/store"
|
||||
"github.com/containous/traefik/v2/pkg/config/parser"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDecodeToNode(t *testing.T) {
|
||||
type expected struct {
|
||||
error bool
|
||||
node *parser.Node
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
in map[string]string
|
||||
filters []string
|
||||
expected expected
|
||||
}{
|
||||
{
|
||||
desc: "no label",
|
||||
in: map[string]string{},
|
||||
expected: expected{node: nil},
|
||||
},
|
||||
{
|
||||
desc: "level 1",
|
||||
in: map[string]string{
|
||||
"traefik/foo": "bar",
|
||||
},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{
|
||||
{Name: "foo", Value: "bar"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
desc: "level 1 empty value",
|
||||
in: map[string]string{
|
||||
"traefik/foo": "",
|
||||
},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{
|
||||
{Name: "foo", Value: ""},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
desc: "level 2",
|
||||
in: map[string]string{
|
||||
"traefik/foo/bar": "bar",
|
||||
},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{{
|
||||
Name: "foo",
|
||||
Children: []*parser.Node{
|
||||
{Name: "bar", Value: "bar"},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
{
|
||||
desc: "several entries, level 0",
|
||||
in: map[string]string{
|
||||
"traefik": "bar",
|
||||
"traefic": "bur",
|
||||
},
|
||||
expected: expected{error: true},
|
||||
},
|
||||
{
|
||||
desc: "several entries, prefix filter",
|
||||
in: map[string]string{
|
||||
"traefik/foo": "bar",
|
||||
"traefik/fii": "bir",
|
||||
},
|
||||
filters: []string{"traefik/Foo"},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{
|
||||
{Name: "foo", Value: "bar"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
desc: "several entries, level 1",
|
||||
in: map[string]string{
|
||||
"traefik/foo": "bar",
|
||||
"traefik/fii": "bur",
|
||||
},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{
|
||||
{Name: "fii", Value: "bur"},
|
||||
{Name: "foo", Value: "bar"},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
desc: "several entries, level 2",
|
||||
in: map[string]string{
|
||||
"traefik/foo/aaa": "bar",
|
||||
"traefik/foo/bbb": "bur",
|
||||
},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{
|
||||
{Name: "foo", Children: []*parser.Node{
|
||||
{Name: "aaa", Value: "bar"},
|
||||
{Name: "bbb", Value: "bur"},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
desc: "several entries, level 2, case insensitive",
|
||||
in: map[string]string{
|
||||
"traefik/foo/aaa": "bar",
|
||||
"traefik/Foo/bbb": "bur",
|
||||
},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{
|
||||
{Name: "Foo", Children: []*parser.Node{
|
||||
{Name: "bbb", Value: "bur"},
|
||||
{Name: "aaa", Value: "bar"},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
desc: "several entries, level 2, 3 children",
|
||||
in: map[string]string{
|
||||
"traefik/foo/aaa": "bar",
|
||||
"traefik/foo/bbb": "bur",
|
||||
"traefik/foo/ccc": "bir",
|
||||
},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{
|
||||
{Name: "foo", Children: []*parser.Node{
|
||||
{Name: "aaa", Value: "bar"},
|
||||
{Name: "bbb", Value: "bur"},
|
||||
{Name: "ccc", Value: "bir"},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
desc: "several entries, level 3",
|
||||
in: map[string]string{
|
||||
"traefik/foo/bar/aaa": "bar",
|
||||
"traefik/foo/bar/bbb": "bur",
|
||||
},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{
|
||||
{Name: "foo", Children: []*parser.Node{
|
||||
{Name: "bar", Children: []*parser.Node{
|
||||
{Name: "aaa", Value: "bar"},
|
||||
{Name: "bbb", Value: "bur"},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
desc: "several entries, level 3, 2 children level 1",
|
||||
in: map[string]string{
|
||||
"traefik/foo/bar/aaa": "bar",
|
||||
"traefik/foo/bar/bbb": "bur",
|
||||
"traefik/bar/foo/bbb": "bir",
|
||||
},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{
|
||||
{Name: "bar", Children: []*parser.Node{
|
||||
{Name: "foo", Children: []*parser.Node{
|
||||
{Name: "bbb", Value: "bir"},
|
||||
}},
|
||||
}},
|
||||
{Name: "foo", Children: []*parser.Node{
|
||||
{Name: "bar", Children: []*parser.Node{
|
||||
{Name: "aaa", Value: "bar"},
|
||||
{Name: "bbb", Value: "bur"},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
desc: "several entries, slice syntax",
|
||||
in: map[string]string{
|
||||
"traefik/foo/0/aaa": "bar0",
|
||||
"traefik/foo/0/bbb": "bur0",
|
||||
"traefik/foo/1/aaa": "bar1",
|
||||
"traefik/foo/1/bbb": "bur1",
|
||||
},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{
|
||||
{Name: "foo", Children: []*parser.Node{
|
||||
{Name: "[0]", Children: []*parser.Node{
|
||||
{Name: "aaa", Value: "bar0"},
|
||||
{Name: "bbb", Value: "bur0"},
|
||||
}},
|
||||
{Name: "[1]", Children: []*parser.Node{
|
||||
{Name: "aaa", Value: "bar1"},
|
||||
{Name: "bbb", Value: "bur1"},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
desc: "several entries, slice in slice of struct",
|
||||
in: map[string]string{
|
||||
"traefik/foo/0/aaa/0": "bar0",
|
||||
"traefik/foo/0/aaa/1": "bar1",
|
||||
"traefik/foo/1/aaa/0": "bar2",
|
||||
"traefik/foo/1/aaa/1": "bar3",
|
||||
},
|
||||
expected: expected{node: &parser.Node{
|
||||
Name: "traefik",
|
||||
Children: []*parser.Node{
|
||||
{Name: "foo", Children: []*parser.Node{
|
||||
{Name: "[0]", Children: []*parser.Node{
|
||||
{Name: "aaa", Value: "bar0,bar1"},
|
||||
}},
|
||||
{Name: "[1]", Children: []*parser.Node{
|
||||
{Name: "aaa", Value: "bar2,bar3"},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
out, err := DecodeToNode(mapToPairs(test.in), "traefik", test.filters...)
|
||||
|
||||
if test.expected.error {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
|
||||
if !assert.Equal(t, test.expected.node, out) {
|
||||
bytes, err := json.MarshalIndent(out, "", " ")
|
||||
require.NoError(t, err)
|
||||
fmt.Println(string(bytes))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func mapToPairs(in map[string]string) []*store.KVPair {
|
||||
var out []*store.KVPair
|
||||
for k, v := range in {
|
||||
out = append(out, &store.KVPair{Key: k, Value: []byte(v)})
|
||||
}
|
||||
return out
|
||||
}
|
63
pkg/config/kv/kv_test.go
Normal file
63
pkg/config/kv/kv_test.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package kv
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
pairs := mapToPairs(map[string]string{
|
||||
"traefik/fielda": "bar",
|
||||
"traefik/fieldb": "1",
|
||||
"traefik/fieldc": "true",
|
||||
"traefik/fieldd/0": "one",
|
||||
"traefik/fieldd/1": "two",
|
||||
"traefik/fielde": "",
|
||||
"traefik/fieldf/Test1": "A",
|
||||
"traefik/fieldf/Test2": "B",
|
||||
"traefik/fieldg/0/name": "A",
|
||||
"traefik/fieldg/1/name": "B",
|
||||
})
|
||||
|
||||
element := &sample{}
|
||||
|
||||
err := Decode(pairs, element, "traefik")
|
||||
require.NoError(t, err)
|
||||
|
||||
expected := &sample{
|
||||
FieldA: "bar",
|
||||
FieldB: 1,
|
||||
FieldC: true,
|
||||
FieldD: []string{"one", "two"},
|
||||
FieldE: &struct {
|
||||
Name string
|
||||
}{},
|
||||
FieldF: map[string]string{
|
||||
"Test1": "A",
|
||||
"Test2": "B",
|
||||
},
|
||||
FieldG: []sub{
|
||||
{Name: "A"},
|
||||
{Name: "B"},
|
||||
},
|
||||
}
|
||||
assert.Equal(t, expected, element)
|
||||
}
|
||||
|
||||
type sample struct {
|
||||
FieldA string
|
||||
FieldB int
|
||||
FieldC bool
|
||||
FieldD []string
|
||||
FieldE *struct {
|
||||
Name string
|
||||
} `label:"allowEmpty"`
|
||||
FieldF map[string]string
|
||||
FieldG []sub
|
||||
}
|
||||
|
||||
type sub struct {
|
||||
Name string
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue