1
0
Fork 0

Add KV store providers (dynamic configuration only)

Co-authored-by: Jean-Baptiste Doumenjou <jb.doumenjou@gmail.com>
This commit is contained in:
Ludovic Fernandez 2019-11-28 21:56:04 +01:00 committed by Traefiker Bot
parent 028683666d
commit 9b9f4be6a4
61 changed files with 5825 additions and 70 deletions

View file

@ -46,17 +46,20 @@ func Encode(element interface{}) ([]parser.Flat, error) {
return nil, nil
}
node, err := parser.EncodeToNode(element, parser.DefaultRootName, false)
etnOpts := parser.EncoderToNodeOpts{OmitEmpty: false, TagName: parser.TagLabel, AllowSliceAsStruct: true}
node, err := parser.EncodeToNode(element, parser.DefaultRootName, etnOpts)
if err != nil {
return nil, err
}
err = parser.AddMetadata(element, node)
metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: true}
err = parser.AddMetadata(element, node, metaOpts)
if err != nil {
return nil, err
}
return parser.EncodeToFlat(element, node, parser.FlatOpts{Case: "upper", Separator: "_"})
flatOpts := parser.FlatOpts{Case: "upper", Separator: "_", TagName: parser.TagLabel}
return parser.EncodeToFlat(element, node, flatOpts)
}
func checkPrefix(prefix string) error {

View file

@ -22,10 +22,11 @@ func Decode(filePath string, element interface{}) error {
return err
}
err = parser.AddMetadata(element, root)
metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: true}
err = parser.AddMetadata(element, root, metaOpts)
if err != nil {
return err
}
return parser.Fill(element, root)
return parser.Fill(element, root, parser.FillerOpts{AllowSliceAsStruct: true})
}

View file

@ -30,15 +30,18 @@ func Encode(element interface{}) ([]parser.Flat, error) {
return nil, nil
}
node, err := parser.EncodeToNode(element, parser.DefaultRootName, false)
etnOpts := parser.EncoderToNodeOpts{OmitEmpty: false, TagName: parser.TagLabel, AllowSliceAsStruct: true}
node, err := parser.EncodeToNode(element, parser.DefaultRootName, etnOpts)
if err != nil {
return nil, err
}
err = parser.AddMetadata(element, node)
metaOpts := parser.MetadataOpts{TagName: parser.TagLabel, AllowSliceAsStruct: true}
err = parser.AddMetadata(element, node, metaOpts)
if err != nil {
return nil, err
}
return parser.EncodeToFlat(element, node, parser.FlatOpts{Separator: ".", SkipRoot: true})
flatOpts := parser.FlatOpts{Separator: ".", SkipRoot: true, TagName: parser.TagLabel}
return parser.EncodeToFlat(element, node, flatOpts)
}

75
pkg/config/kv/kv.go Normal file
View 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
View 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
}

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

View file

@ -14,8 +14,22 @@ type initializer interface {
SetDefaults()
}
// FillerOpts Options for the filler.
type FillerOpts struct {
AllowSliceAsStruct bool
}
// Fill populates the fields of the element using the information in node.
func Fill(element interface{}, node *Node) error {
func Fill(element interface{}, node *Node, opts FillerOpts) error {
return filler{FillerOpts: opts}.Fill(element, node)
}
type filler struct {
FillerOpts
}
// Fill populates the fields of the element using the information in node.
func (f filler) Fill(element interface{}, node *Node) error {
if element == nil || node == nil {
return nil
}
@ -29,10 +43,10 @@ func Fill(element interface{}, node *Node) error {
return fmt.Errorf("struct are not supported, use pointer instead")
}
return fill(root.Elem(), node)
return f.fill(root.Elem(), node)
}
func fill(field reflect.Value, node *Node) error {
func (f filler) fill(field reflect.Value, node *Node) error {
// related to allow-empty tag
if node.Disabled {
return nil
@ -70,19 +84,19 @@ func fill(field reflect.Value, node *Node) error {
case reflect.Float64:
return setFloat(field, node.Value, 64)
case reflect.Struct:
return setStruct(field, node)
return f.setStruct(field, node)
case reflect.Ptr:
return setPtr(field, node)
return f.setPtr(field, node)
case reflect.Map:
return setMap(field, node)
return f.setMap(field, node)
case reflect.Slice:
return setSlice(field, node)
return f.setSlice(field, node)
default:
return nil
}
}
func setPtr(field reflect.Value, node *Node) error {
func (f filler) setPtr(field reflect.Value, node *Node) error {
if field.IsNil() {
field.Set(reflect.New(field.Type().Elem()))
@ -94,10 +108,10 @@ func setPtr(field reflect.Value, node *Node) error {
}
}
return fill(field.Elem(), node)
return f.fill(field.Elem(), node)
}
func setStruct(field reflect.Value, node *Node) error {
func (f filler) setStruct(field reflect.Value, node *Node) error {
for _, child := range node.Children {
fd := field.FieldByName(child.FieldName)
@ -106,7 +120,7 @@ func setStruct(field reflect.Value, node *Node) error {
return fmt.Errorf("field not found, node: %s (%s)", child.Name, child.FieldName)
}
err := fill(fd, child)
err := f.fill(fd, child)
if err != nil {
return err
}
@ -115,10 +129,10 @@ func setStruct(field reflect.Value, node *Node) error {
return nil
}
func setSlice(field reflect.Value, node *Node) error {
func (f filler) setSlice(field reflect.Value, node *Node) error {
if field.Type().Elem().Kind() == reflect.Struct ||
field.Type().Elem().Kind() == reflect.Ptr && field.Type().Elem().Elem().Kind() == reflect.Struct {
return setSliceStruct(field, node)
return f.setSliceStruct(field, node)
}
if len(node.Value) == 0 {
@ -211,9 +225,9 @@ func setSlice(field reflect.Value, node *Node) error {
return nil
}
func setSliceStruct(field reflect.Value, node *Node) error {
if node.Tag.Get(TagLabelSliceAsStruct) != "" {
return setSliceAsStruct(field, node)
func (f filler) setSliceStruct(field reflect.Value, node *Node) error {
if f.AllowSliceAsStruct && node.Tag.Get(TagLabelSliceAsStruct) != "" {
return f.setSliceAsStruct(field, node)
}
field.Set(reflect.MakeSlice(field.Type(), len(node.Children), len(node.Children)))
@ -221,7 +235,7 @@ func setSliceStruct(field reflect.Value, node *Node) error {
for i, child := range node.Children {
// use Ptr to allow "SetDefaults"
value := reflect.New(reflect.PtrTo(field.Type().Elem()))
err := setPtr(value, child)
err := f.setPtr(value, child)
if err != nil {
return err
}
@ -232,14 +246,14 @@ func setSliceStruct(field reflect.Value, node *Node) error {
return nil
}
func setSliceAsStruct(field reflect.Value, node *Node) error {
func (f filler) setSliceAsStruct(field reflect.Value, node *Node) error {
if len(node.Children) == 0 {
return fmt.Errorf("invalid slice: node %s", node.Name)
}
// use Ptr to allow "SetDefaults"
value := reflect.New(reflect.PtrTo(field.Type().Elem()))
err := setPtr(value, node)
err := f.setPtr(value, node)
if err != nil {
return err
}
@ -252,7 +266,7 @@ func setSliceAsStruct(field reflect.Value, node *Node) error {
return nil
}
func setMap(field reflect.Value, node *Node) error {
func (f filler) setMap(field reflect.Value, node *Node) error {
if field.IsNil() {
field.Set(reflect.MakeMap(field.Type()))
}
@ -260,7 +274,7 @@ func setMap(field reflect.Value, node *Node) error {
for _, child := range node.Children {
ptrValue := reflect.New(reflect.PtrTo(field.Type().Elem()))
err := fill(ptrValue, child)
err := f.fill(ptrValue, child)
if err != nil {
return err
}

View file

@ -1390,7 +1390,7 @@ func TestFill(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
err := Fill(test.element, test.node)
err := filler{FillerOpts: FillerOpts{AllowSliceAsStruct: true}}.Fill(test.element, test.node)
if test.expected.error {
require.Error(t, err)
} else {

View file

@ -7,13 +7,20 @@ import (
"strings"
)
// EncoderToNodeOpts Options for the encoderToNode.
type EncoderToNodeOpts struct {
OmitEmpty bool
TagName string
AllowSliceAsStruct bool
}
// EncodeToNode converts an element to a node.
// element -> nodes
func EncodeToNode(element interface{}, rootName string, omitEmpty bool) (*Node, error) {
func EncodeToNode(element interface{}, rootName string, opts EncoderToNodeOpts) (*Node, error) {
rValue := reflect.ValueOf(element)
node := &Node{Name: rootName}
encoder := encoderToNode{omitEmpty: omitEmpty}
encoder := encoderToNode{EncoderToNodeOpts: opts}
err := encoder.setNodeValue(node, rValue)
if err != nil {
@ -24,7 +31,7 @@ func EncodeToNode(element interface{}, rootName string, omitEmpty bool) (*Node,
}
type encoderToNode struct {
omitEmpty bool
EncoderToNodeOpts
}
func (e encoderToNode) setNodeValue(node *Node, rValue reflect.Value) error {
@ -65,7 +72,7 @@ func (e encoderToNode) setStructValue(node *Node, rValue reflect.Value) error {
continue
}
if field.Tag.Get(TagLabel) == "-" {
if field.Tag.Get(e.TagName) == "-" {
continue
}
@ -78,7 +85,7 @@ func (e encoderToNode) setStructValue(node *Node, rValue reflect.Value) error {
}
nodeName := field.Name
if field.Type.Kind() == reflect.Slice && len(field.Tag.Get(TagLabelSliceAsStruct)) != 0 {
if e.AllowSliceAsStruct && field.Type.Kind() == reflect.Slice && len(field.Tag.Get(TagLabelSliceAsStruct)) != 0 {
nodeName = field.Tag.Get(TagLabelSliceAsStruct)
}
@ -101,7 +108,7 @@ func (e encoderToNode) setStructValue(node *Node, rValue reflect.Value) error {
}
if field.Type.Elem().Kind() == reflect.Struct && len(child.Children) == 0 {
if field.Tag.Get(TagLabel) != TagLabelAllowEmpty {
if field.Tag.Get(e.TagName) != TagLabelAllowEmpty {
continue
}
@ -181,7 +188,7 @@ func (e encoderToNode) setSliceValue(node *Node, rValue reflect.Value) error {
}
func (e encoderToNode) isSkippedField(field reflect.StructField, fieldValue reflect.Value) bool {
if e.omitEmpty && field.Type.Kind() == reflect.String && fieldValue.Len() == 0 {
if e.OmitEmpty && field.Type.Kind() == reflect.String && fieldValue.Len() == 0 {
return true
}
@ -189,7 +196,7 @@ func (e encoderToNode) isSkippedField(field reflect.StructField, fieldValue refl
return true
}
if e.omitEmpty && (field.Type.Kind() == reflect.Slice) &&
if e.OmitEmpty && (field.Type.Kind() == reflect.Slice) &&
(fieldValue.IsNil() || fieldValue.Len() == 0) {
return true
}

View file

@ -723,7 +723,8 @@ func TestEncodeToNode(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
node, err := EncodeToNode(test.element, DefaultRootName, true)
etnOpts := EncoderToNodeOpts{OmitEmpty: true, TagName: TagLabel, AllowSliceAsStruct: true}
node, err := EncodeToNode(test.element, DefaultRootName, etnOpts)
if test.expected.error {
require.Error(t, err)

View file

@ -18,6 +18,7 @@ type FlatOpts struct {
Case string // "lower" or "upper", defaults to "lower".
Separator string
SkipRoot bool
TagName string
}
// Flat is a configuration item representation.
@ -69,7 +70,7 @@ func (e encoderToFlat) createFlat(field reflect.Value, name string, node *Node)
var entries []Flat
if node.Kind != reflect.Map && node.Description != "-" {
if !(node.Kind == reflect.Ptr && len(node.Children) > 0) ||
(node.Kind == reflect.Ptr && node.Tag.Get("label") == TagLabelAllowEmpty) {
(node.Kind == reflect.Ptr && node.Tag.Get(e.TagName) == TagLabelAllowEmpty) {
if node.Name[0] != '[' {
entries = append(entries, Flat{
Name: e.getName(name),

View file

@ -156,6 +156,7 @@ func TestEncodeToFlat(t *testing.T) {
Case: "upper",
Separator: "_",
SkipRoot: false,
TagName: TagLabel,
},
expected: []Flat{{
Name: "TRAEFIK_FIELD",
@ -1236,7 +1237,7 @@ func TestEncodeToFlat(t *testing.T) {
var opts FlatOpts
if test.opts == nil {
opts = FlatOpts{Separator: ".", SkipRoot: true}
opts = FlatOpts{Separator: ".", SkipRoot: true, TagName: TagLabel}
} else {
opts = *test.opts
}

View file

@ -7,8 +7,23 @@ import (
"strings"
)
// MetadataOpts Options for the metadata.
type MetadataOpts struct {
TagName string
AllowSliceAsStruct bool
}
// AddMetadata adds metadata such as type, inferred from element, to a node.
func AddMetadata(element interface{}, node *Node) error {
func AddMetadata(element interface{}, node *Node, opts MetadataOpts) error {
return metadata{MetadataOpts: opts}.Add(element, node)
}
type metadata struct {
MetadataOpts
}
// Add adds metadata such as type, inferred from element, to a node.
func (m metadata) Add(element interface{}, node *Node) error {
if node == nil {
return nil
}
@ -24,25 +39,25 @@ func AddMetadata(element interface{}, node *Node) error {
rootType := reflect.TypeOf(element)
node.Kind = rootType.Kind()
return browseChildren(rootType, node)
return m.browseChildren(rootType, node)
}
func browseChildren(fType reflect.Type, node *Node) error {
func (m metadata) browseChildren(fType reflect.Type, node *Node) error {
for _, child := range node.Children {
if err := addMetadata(fType, child); err != nil {
if err := m.add(fType, child); err != nil {
return err
}
}
return nil
}
func addMetadata(rootType reflect.Type, node *Node) error {
func (m metadata) add(rootType reflect.Type, node *Node) error {
rType := rootType
if rootType.Kind() == reflect.Ptr {
rType = rootType.Elem()
}
field, err := findTypedField(rType, node)
field, err := m.findTypedField(rType, node)
if err != nil {
return err
}
@ -57,11 +72,11 @@ func addMetadata(rootType reflect.Type, node *Node) error {
if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Ptr && fType.Elem().Kind() == reflect.Struct ||
fType.Kind() == reflect.Map {
if len(node.Children) == 0 && field.Tag.Get(TagLabel) != TagLabelAllowEmpty {
if len(node.Children) == 0 && field.Tag.Get(m.TagName) != TagLabelAllowEmpty {
return fmt.Errorf("%s cannot be a standalone element (type %s)", node.Name, fType)
}
node.Disabled = len(node.Value) > 0 && !strings.EqualFold(node.Value, "true") && field.Tag.Get(TagLabel) == TagLabelAllowEmpty
node.Disabled = len(node.Value) > 0 && !strings.EqualFold(node.Value, "true") && field.Tag.Get(m.TagName) == TagLabelAllowEmpty
}
if len(node.Children) == 0 {
@ -69,7 +84,7 @@ func addMetadata(rootType reflect.Type, node *Node) error {
}
if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Ptr && fType.Elem().Kind() == reflect.Struct {
return browseChildren(fType, node)
return m.browseChildren(fType, node)
}
if fType.Kind() == reflect.Map {
@ -80,7 +95,7 @@ func addMetadata(rootType reflect.Type, node *Node) error {
if elem.Kind() == reflect.Map || elem.Kind() == reflect.Struct ||
(elem.Kind() == reflect.Ptr && elem.Elem().Kind() == reflect.Struct) {
if err = browseChildren(elem, child); err != nil {
if err = m.browseChildren(elem, child); err != nil {
return err
}
}
@ -89,13 +104,13 @@ func addMetadata(rootType reflect.Type, node *Node) error {
}
if fType.Kind() == reflect.Slice {
if field.Tag.Get(TagLabelSliceAsStruct) != "" {
return browseChildren(fType.Elem(), node)
if m.AllowSliceAsStruct && field.Tag.Get(TagLabelSliceAsStruct) != "" {
return m.browseChildren(fType.Elem(), node)
}
for _, ch := range node.Children {
ch.Kind = fType.Elem().Kind()
if err = browseChildren(fType.Elem(), ch); err != nil {
if err = m.browseChildren(fType.Elem(), ch); err != nil {
return err
}
}
@ -105,19 +120,19 @@ func addMetadata(rootType reflect.Type, node *Node) error {
return fmt.Errorf("invalid node %s: %v", node.Name, fType.Kind())
}
func findTypedField(rType reflect.Type, node *Node) (reflect.StructField, error) {
func (m metadata) findTypedField(rType reflect.Type, node *Node) (reflect.StructField, error) {
for i := 0; i < rType.NumField(); i++ {
cField := rType.Field(i)
fieldName := cField.Tag.Get(TagLabelSliceAsStruct)
if len(fieldName) == 0 {
if !m.AllowSliceAsStruct || len(fieldName) == 0 {
fieldName = cField.Name
}
if IsExported(cField) {
if cField.Anonymous {
if cField.Type.Kind() == reflect.Struct {
structField, err := findTypedField(cField.Type, node)
structField, err := m.findTypedField(cField.Type, node)
if err != nil {
continue
}

View file

@ -991,7 +991,7 @@ func TestAddMetadata(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
err := AddMetadata(test.structure, test.tree)
err := metadata{MetadataOpts{TagName: TagLabel, AllowSliceAsStruct: true}}.Add(test.structure, test.tree)
if test.expected.error {
assert.Error(t, err)

View file

@ -13,12 +13,13 @@ func Decode(labels map[string]string, element interface{}, rootName string, filt
return err
}
err = AddMetadata(element, node)
metaOpts := MetadataOpts{TagName: TagLabel, AllowSliceAsStruct: true}
err = AddMetadata(element, node, metaOpts)
if err != nil {
return err
}
err = Fill(element, node)
err = Fill(element, node, FillerOpts{AllowSliceAsStruct: true})
if err != nil {
return err
}
@ -29,7 +30,8 @@ func Decode(labels map[string]string, element interface{}, rootName string, filt
// Encode converts an element to labels.
// element -> node (value) -> label (node)
func Encode(element interface{}, rootName string) (map[string]string, error) {
node, err := EncodeToNode(element, rootName, true)
etnOpts := EncoderToNodeOpts{OmitEmpty: true, TagName: TagLabel, AllowSliceAsStruct: true}
node, err := EncodeToNode(element, rootName, etnOpts)
if err != nil {
return nil, err
}

View file

@ -14,6 +14,10 @@ import (
"github.com/containous/traefik/v2/pkg/provider/file"
"github.com/containous/traefik/v2/pkg/provider/kubernetes/crd"
"github.com/containous/traefik/v2/pkg/provider/kubernetes/ingress"
"github.com/containous/traefik/v2/pkg/provider/kv/consul"
"github.com/containous/traefik/v2/pkg/provider/kv/etcd"
"github.com/containous/traefik/v2/pkg/provider/kv/redis"
"github.com/containous/traefik/v2/pkg/provider/kv/zk"
"github.com/containous/traefik/v2/pkg/provider/marathon"
"github.com/containous/traefik/v2/pkg/provider/rancher"
"github.com/containous/traefik/v2/pkg/provider/rest"
@ -156,15 +160,21 @@ func (t *Tracing) SetDefaults() {
// Providers contains providers configuration
type Providers struct {
ProvidersThrottleDuration types.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"`
Docker *docker.Provider `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" export:"true" label:"allowEmpty"`
File *file.Provider `description:"Enable File backend with default settings." json:"file,omitempty" toml:"file,omitempty" yaml:"file,omitempty" export:"true"`
Marathon *marathon.Provider `description:"Enable Marathon backend with default settings." json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" export:"true" label:"allowEmpty"`
KubernetesIngress *ingress.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" export:"true" label:"allowEmpty"`
KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" export:"true" label:"allowEmpty"`
Rest *rest.Provider `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" export:"true" label:"allowEmpty"`
Rancher *rancher.Provider `description:"Enable Rancher backend with default settings." json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" export:"true" label:"allowEmpty"`
ConsulCatalog *consulcatalog.Provider `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty"`
ProvidersThrottleDuration types.Duration `description:"Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time." json:"providersThrottleDuration,omitempty" toml:"providersThrottleDuration,omitempty" yaml:"providersThrottleDuration,omitempty" export:"true"`
Docker *docker.Provider `description:"Enable Docker backend with default settings." json:"docker,omitempty" toml:"docker,omitempty" yaml:"docker,omitempty" export:"true" label:"allowEmpty"`
File *file.Provider `description:"Enable File backend with default settings." json:"file,omitempty" toml:"file,omitempty" yaml:"file,omitempty" export:"true"`
Marathon *marathon.Provider `description:"Enable Marathon backend with default settings." json:"marathon,omitempty" toml:"marathon,omitempty" yaml:"marathon,omitempty" export:"true" label:"allowEmpty"`
KubernetesIngress *ingress.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesIngress,omitempty" toml:"kubernetesIngress,omitempty" yaml:"kubernetesIngress,omitempty" export:"true" label:"allowEmpty"`
KubernetesCRD *crd.Provider `description:"Enable Kubernetes backend with default settings." json:"kubernetesCRD,omitempty" toml:"kubernetesCRD,omitempty" yaml:"kubernetesCRD,omitempty" export:"true" label:"allowEmpty"`
Rest *rest.Provider `description:"Enable Rest backend with default settings." json:"rest,omitempty" toml:"rest,omitempty" yaml:"rest,omitempty" export:"true" label:"allowEmpty"`
Rancher *rancher.Provider `description:"Enable Rancher backend with default settings." json:"rancher,omitempty" toml:"rancher,omitempty" yaml:"rancher,omitempty" export:"true" label:"allowEmpty"`
ConsulCatalog *consulcatalog.Provider `description:"Enable ConsulCatalog backend with default settings." json:"consulCatalog,omitempty" toml:"consulCatalog,omitempty" yaml:"consulCatalog,omitempty"`
Consul *consul.Provider `description:"Enable Consul backend with default settings." json:"consul,omitempty" toml:"consul,omitempty" yaml:"consul,omitempty" export:"true" label:"allowEmpty"`
Etcd *etcd.Provider `description:"Enable Etcd backend with default settings." json:"etcd,omitempty" toml:"etcd,omitempty" yaml:"etcd,omitempty" export:"true" label:"allowEmpty"`
ZooKeeper *zk.Provider `description:"Enable ZooKeeper backend with default settings." json:"zooKeeper,omitempty" toml:"zooKeeper,omitempty" yaml:"zooKeeper,omitempty" export:"true" label:"allowEmpty"`
Redis *redis.Provider `description:"Enable Redis backend with default settings." json:"redis,omitempty" toml:"redis,omitempty" yaml:"redis,omitempty" export:"true" label:"allowEmpty"`
}
// SetEffectiveConfiguration adds missing configuration parameters derived from existing ones.