From 11a0078966152fcad8a79f977b0f8e2dc4f6b907 Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Tue, 4 Dec 2018 14:24:04 +0100 Subject: [PATCH] Labels parser. --- config/dyn_config.go | 6 +- config/middlewares.go | 2 +- provider/label/internal/element_fill.go | 266 ++++ provider/label/internal/element_fill_test.go | 1088 +++++++++++++++++ provider/label/internal/element_nodes.go | 163 +++ provider/label/internal/element_nodes_test.go | 593 +++++++++ provider/label/internal/labels_decode.go | 68 ++ provider/label/internal/labels_decode_test.go | 190 +++ provider/label/internal/labels_encode.go | 24 + provider/label/internal/labels_encode_test.go | 156 +++ provider/label/internal/node.go | 13 + provider/label/internal/nodes_metadata.go | 164 +++ .../label/internal/nodes_metadata_test.go | 769 ++++++++++++ provider/label/internal/tags.go | 12 + provider/label/parser.go | 39 + provider/label/parser_test.go | 913 ++++++++++++++ 16 files changed, 4462 insertions(+), 4 deletions(-) create mode 100644 provider/label/internal/element_fill.go create mode 100644 provider/label/internal/element_fill_test.go create mode 100644 provider/label/internal/element_nodes.go create mode 100644 provider/label/internal/element_nodes_test.go create mode 100644 provider/label/internal/labels_decode.go create mode 100644 provider/label/internal/labels_decode_test.go create mode 100644 provider/label/internal/labels_encode.go create mode 100644 provider/label/internal/labels_encode_test.go create mode 100644 provider/label/internal/node.go create mode 100644 provider/label/internal/nodes_metadata.go create mode 100644 provider/label/internal/nodes_metadata_test.go create mode 100644 provider/label/internal/tags.go create mode 100644 provider/label/parser.go create mode 100644 provider/label/parser_test.go diff --git a/config/dyn_config.go b/config/dyn_config.go index 1a7ed5d50..c05e7ae10 100644 --- a/config/dyn_config.go +++ b/config/dyn_config.go @@ -21,8 +21,8 @@ type Router struct { // LoadBalancerService holds the LoadBalancerService configuration. type LoadBalancerService struct { - Stickiness *Stickiness `json:"stickiness,omitempty" toml:",omitempty"` - Servers []Server `json:"servers,omitempty" toml:",omitempty"` + Stickiness *Stickiness `json:"stickiness,omitempty" toml:",omitempty" label:"allowEmpty"` + Servers []Server `json:"servers,omitempty" toml:",omitempty" label-slice-as-struct:"server"` Method string `json:"method,omitempty" toml:",omitempty"` HealthCheck *HealthCheck `json:"healthCheck,omitempty" toml:",omitempty"` PassHostHeader bool `json:"passHostHeader" toml:",omitempty"` @@ -151,7 +151,7 @@ type Configuration struct { Routers map[string]*Router `json:"routers,omitempty" toml:",omitempty"` Middlewares map[string]*Middleware `json:"middlewares,omitempty" toml:",omitempty"` Services map[string]*Service `json:"services,omitempty" toml:",omitempty"` - TLS []*traefiktls.Configuration `json:"-"` + TLS []*traefiktls.Configuration `json:"-" label:"-"` } // Service holds a service configuration (can only be of one type at the same time). diff --git a/config/middlewares.go b/config/middlewares.go index cc0984bef..f9edd60fb 100644 --- a/config/middlewares.go +++ b/config/middlewares.go @@ -24,7 +24,7 @@ type Middleware struct { MaxConn *MaxConn `json:"maxConn,omitempty"` Buffering *Buffering `json:"buffering,omitempty"` CircuitBreaker *CircuitBreaker `json:"circuitBreaker,omitempty"` - Compress *Compress `json:"compress,omitempty"` + Compress *Compress `json:"compress,omitempty" label:"allowEmpty"` PassTLSClientCert *PassTLSClientCert `json:"passTLSClientCert,omitempty"` Retry *Retry `json:"retry,omitempty"` } diff --git a/provider/label/internal/element_fill.go b/provider/label/internal/element_fill.go new file mode 100644 index 000000000..560d988dd --- /dev/null +++ b/provider/label/internal/element_fill.go @@ -0,0 +1,266 @@ +package internal + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// Fill the fields of the element. +// nodes -> element +func Fill(element interface{}, node *Node) error { + if element == nil || node == nil { + return nil + } + + if node.Kind == 0 { + return fmt.Errorf("missing node type: %s", node.Name) + } + + elem := reflect.ValueOf(element) + if elem.Kind() == reflect.Struct { + return fmt.Errorf("struct are not supported, use pointer instead") + } + + return fill(elem.Elem(), node) +} + +func fill(field reflect.Value, node *Node) error { + // related to allow-empty tag + if node.Disabled { + return nil + } + + switch field.Kind() { + case reflect.String: + field.SetString(node.Value) + return nil + case reflect.Bool: + val, err := strconv.ParseBool(node.Value) + if err != nil { + return err + } + field.SetBool(val) + return nil + case reflect.Int8: + return setInt(field, node.Value, 8) + case reflect.Int16: + return setInt(field, node.Value, 16) + case reflect.Int32: + return setInt(field, node.Value, 32) + case reflect.Int64, reflect.Int: + return setInt(field, node.Value, 64) + case reflect.Uint8: + return setUint(field, node.Value, 8) + case reflect.Uint16: + return setUint(field, node.Value, 16) + case reflect.Uint32: + return setUint(field, node.Value, 32) + case reflect.Uint64, reflect.Uint: + return setUint(field, node.Value, 64) + case reflect.Float32: + return setFloat(field, node.Value, 32) + case reflect.Float64: + return setFloat(field, node.Value, 64) + case reflect.Struct: + return setStruct(field, node) + case reflect.Ptr: + return setPtr(field, node) + case reflect.Map: + return setMap(field, node) + case reflect.Slice: + return setSlice(field, node) + default: + return nil + } +} + +func setPtr(field reflect.Value, node *Node) error { + if field.IsNil() { + field.Set(reflect.New(field.Type().Elem())) + } + + return fill(field.Elem(), node) +} + +func setStruct(field reflect.Value, node *Node) error { + for _, child := range node.Children { + fd := field.FieldByName(child.FieldName) + + zeroValue := reflect.Value{} + if fd == zeroValue { + return fmt.Errorf("field not found, node: %s (%s)", child.Name, child.FieldName) + } + + err := fill(fd, child) + if err != nil { + return err + } + } + + return nil +} + +func setSlice(field reflect.Value, node *Node) error { + if field.Type().Elem().Kind() == reflect.Struct { + return setSliceAsStruct(field, node) + } + + if len(node.Value) == 0 { + return nil + } + + values := strings.Split(node.Value, ",") + + slice := reflect.MakeSlice(field.Type(), len(values), len(values)) + field.Set(slice) + + for i := 0; i < len(values); i++ { + value := strings.TrimSpace(values[i]) + + switch field.Type().Elem().Kind() { + case reflect.String: + field.Index(i).Set(reflect.ValueOf(value)) + case reflect.Int: + val, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + field.Index(i).SetInt(val) + case reflect.Int8: + err := setInt(field.Index(i), value, 8) + if err != nil { + return err + } + case reflect.Int16: + err := setInt(field.Index(i), value, 16) + if err != nil { + return err + } + case reflect.Int32: + err := setInt(field.Index(i), value, 32) + if err != nil { + return err + } + case reflect.Int64: + err := setInt(field.Index(i), value, 64) + if err != nil { + return err + } + case reflect.Uint: + val, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return err + } + field.Index(i).SetUint(val) + case reflect.Uint8: + err := setUint(field.Index(i), value, 8) + if err != nil { + return err + } + case reflect.Uint16: + err := setUint(field.Index(i), value, 16) + if err != nil { + return err + } + case reflect.Uint32: + err := setUint(field.Index(i), value, 32) + if err != nil { + return err + } + case reflect.Uint64: + err := setUint(field.Index(i), value, 64) + if err != nil { + return err + } + case reflect.Float32: + err := setFloat(field.Index(i), value, 32) + if err != nil { + return err + } + case reflect.Float64: + err := setFloat(field.Index(i), value, 64) + if err != nil { + return err + } + case reflect.Bool: + val, err := strconv.ParseBool(value) + if err != nil { + return err + } + field.Index(i).SetBool(val) + default: + return fmt.Errorf("unsupported type: %s", field.Type().Elem()) + } + } + return nil +} + +func setSliceAsStruct(field reflect.Value, node *Node) error { + if len(node.Children) == 0 { + return fmt.Errorf("invalid slice: node %s", node.Name) + } + + elem := reflect.New(field.Type().Elem()).Elem() + err := setStruct(elem, node) + if err != nil { + return err + } + + field.Set(reflect.MakeSlice(field.Type(), 1, 1)) + field.Index(0).Set(elem) + + return nil +} + +func setMap(field reflect.Value, node *Node) error { + if field.IsNil() { + field.Set(reflect.MakeMap(field.Type())) + } + + for _, child := range node.Children { + ptrValue := reflect.New(reflect.PtrTo(field.Type().Elem())) + + err := fill(ptrValue, child) + if err != nil { + return err + } + + value := ptrValue.Elem().Elem() + + key := reflect.ValueOf(child.Name) + field.SetMapIndex(key, value) + } + return nil +} + +func setInt(field reflect.Value, value string, bitSize int) error { + val, err := strconv.ParseInt(value, 10, bitSize) + if err != nil { + return err + } + + field.Set(reflect.ValueOf(val).Convert(field.Type())) + return nil +} + +func setUint(field reflect.Value, value string, bitSize int) error { + val, err := strconv.ParseUint(value, 10, bitSize) + if err != nil { + return err + } + + field.Set(reflect.ValueOf(val).Convert(field.Type())) + return nil +} + +func setFloat(field reflect.Value, value string, bitSize int) error { + val, err := strconv.ParseFloat(value, bitSize) + if err != nil { + return err + } + + field.Set(reflect.ValueOf(val).Convert(field.Type())) + return nil +} diff --git a/provider/label/internal/element_fill_test.go b/provider/label/internal/element_fill_test.go new file mode 100644 index 000000000..7924d381a --- /dev/null +++ b/provider/label/internal/element_fill_test.go @@ -0,0 +1,1088 @@ +package internal + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFill(t *testing.T) { + type expected struct { + element interface{} + error bool + } + + testCases := []struct { + desc string + node *Node + element interface{} + expected expected + }{ + { + desc: "empty node", + node: &Node{}, + element: &struct{ Foo string }{}, + expected: expected{error: true}, + }, + { + desc: "empty element", + node: &Node{Name: "traefik", Kind: reflect.Struct}, + element: &struct{}{}, + expected: expected{element: &struct{}{}}, + }, + { + desc: "type struct as root", + node: &Node{Name: "traefik", Kind: reflect.Struct}, + element: struct{}{}, + expected: expected{error: true}, + }, + { + desc: "nil node", + node: nil, + element: &struct{ Foo string }{}, + expected: expected{element: &struct{ Foo string }{}}, + }, + { + desc: "nil element", + node: &Node{Name: "traefik", Kind: reflect.Struct}, + element: nil, + expected: expected{element: nil}, + }, + { + desc: "string", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, + }, + }, + element: &struct{ Foo string }{}, + expected: expected{element: &struct{ Foo string }{Foo: "bar"}}, + }, + { + desc: "field not found", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Fii", Value: "bar", Kind: reflect.String}, + }, + }, + element: &struct{ Foo string }{}, + expected: expected{error: true}, + }, + { + desc: "2 children", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "bir", Kind: reflect.String}, + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int}, + }, + }, + element: &struct { + Fii string + Foo int + }{}, + expected: expected{element: &struct { + Fii string + Foo int + }{Fii: "bir", Foo: 4}}, + }, + { + desc: "case insensitive", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "foo", FieldName: "Foo", Value: "bir", Kind: reflect.String}, + }, + }, + element: &struct { + Foo string + foo int + }{}, + expected: expected{element: &struct { + Foo string + foo int + }{Foo: "bir"}}, + }, + { + desc: "func", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Func}, + }, + }, + element: &struct{ Foo func() }{}, + expected: expected{element: &struct{ Foo func() }{}}, + }, + { + desc: "int", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int}, + }, + }, + element: &struct{ Foo int }{}, + expected: expected{element: &struct{ Foo int }{Foo: 4}}, + }, + { + desc: "invalid int", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int}, + }, + }, + element: &struct{ Foo int }{}, + expected: expected{error: true}, + }, + { + desc: "int8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int8}, + }, + }, + element: &struct{ Foo int8 }{}, + expected: expected{element: &struct{ Foo int8 }{Foo: 4}}, + }, + { + desc: "invalid int8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int8}, + }, + }, + element: &struct{ Foo int8 }{}, + expected: expected{error: true}, + }, + { + desc: "int16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int16}, + }, + }, + element: &struct{ Foo int16 }{}, + expected: expected{element: &struct{ Foo int16 }{Foo: 4}}, + }, + { + desc: "invalid int16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int16}, + }, + }, + element: &struct{ Foo int16 }{}, + expected: expected{error: true}, + }, + { + desc: "int32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int32}, + }, + }, + element: &struct{ Foo int32 }{}, + expected: expected{element: &struct{ Foo int32 }{Foo: 4}}, + }, + { + desc: "invalid int32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int32}, + }, + }, + element: &struct{ Foo int32 }{}, + expected: expected{error: true}, + }, + { + desc: "int64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Int64}, + }, + }, + element: &struct{ Foo int64 }{}, + expected: expected{element: &struct{ Foo int64 }{Foo: 4}}, + }, + { + desc: "invalid int64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Int64}, + }, + }, + element: &struct{ Foo int64 }{}, + expected: expected{error: true}, + }, + { + desc: "uint", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint}, + }, + }, + element: &struct{ Foo uint }{}, + expected: expected{element: &struct{ Foo uint }{Foo: 4}}, + }, + { + desc: "invalid uint", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint}, + }, + }, + element: &struct{ Foo uint }{}, + expected: expected{error: true}, + }, + { + desc: "uint8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint8}, + }, + }, + element: &struct{ Foo uint8 }{}, + expected: expected{element: &struct{ Foo uint8 }{Foo: 4}}, + }, + { + desc: "invalid uint8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint8}, + }, + }, + element: &struct{ Foo uint8 }{}, + expected: expected{error: true}, + }, + { + desc: "uint16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint16}, + }, + }, + element: &struct{ Foo uint16 }{}, + expected: expected{element: &struct{ Foo uint16 }{Foo: 4}}, + }, + { + desc: "invalid uint16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint16}, + }, + }, + element: &struct{ Foo uint16 }{}, + expected: expected{error: true}, + }, + { + desc: "uint32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint32}, + }, + }, + element: &struct{ Foo uint32 }{}, + expected: expected{element: &struct{ Foo uint32 }{Foo: 4}}, + }, + { + desc: "invalid uint32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint32}, + }, + }, + element: &struct{ Foo uint32 }{}, + expected: expected{error: true}, + }, + { + desc: "uint64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4", Kind: reflect.Uint64}, + }, + }, + element: &struct{ Foo uint64 }{}, + expected: expected{element: &struct{ Foo uint64 }{Foo: 4}}, + }, + { + desc: "invalid uint64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four", Kind: reflect.Uint64}, + }, + }, + element: &struct{ Foo uint64 }{}, + expected: expected{error: true}, + }, + { + desc: "bool", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "true", Kind: reflect.Bool}, + }, + }, + element: &struct{ Foo bool }{}, + expected: expected{element: &struct{ Foo bool }{Foo: true}}, + }, + { + desc: "invalid bool", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bool", Kind: reflect.Bool}, + }, + }, + element: &struct{ Foo bool }{}, + expected: expected{error: true}, + }, + { + desc: "float32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2.1", Kind: reflect.Float32}, + }, + }, + element: &struct{ Foo float32 }{}, + expected: expected{element: &struct{ Foo float32 }{Foo: 2.1}}, + }, + { + desc: "invalid float32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "two dot one", Kind: reflect.Float32}, + }, + }, + element: &struct{ Foo float32 }{}, + expected: expected{error: true}, + }, + { + desc: "float64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2.1", Kind: reflect.Float64}, + }, + }, + element: &struct{ Foo float64 }{}, + expected: expected{element: &struct{ Foo float64 }{Foo: 2.1}}, + }, + { + desc: "invalid float64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "two dot one", Kind: reflect.Float64}, + }, + }, + element: &struct{ Foo float64 }{}, + expected: expected{error: true}, + }, + { + desc: "struct", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "huu", Kind: reflect.String}, + {Name: "Fuu", FieldName: "Fuu", Value: "6", Kind: reflect.Int}, + }}, + }}, + element: &struct { + Foo struct { + Fii string + Fuu int + } + }{}, + expected: expected{element: &struct { + Foo struct { + Fii string + Fuu int + } + }{ + Foo: struct { + Fii string + Fuu int + }{ + Fii: "huu", + Fuu: 6, + }}, + }, + }, + { + desc: "pointer", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "huu", Kind: reflect.String}, + {Name: "Fuu", FieldName: "Fuu", Value: "6", Kind: reflect.Int}, + }}, + }}, + element: &struct { + Foo *struct { + Fii string + Fuu int + } + }{}, + expected: expected{element: &struct { + Foo *struct { + Fii string + Fuu int + } + }{ + Foo: &struct { + Fii string + Fuu int + }{ + Fii: "huu", + Fuu: 6, + }}, + }, + }, + { + desc: "pointer disabled false without children", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Ptr, + }, + }}, + element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{}, + expected: expected{element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{ + Foo: &struct { + Fii string + Fuu int + }{}}, + }, + }, + { + desc: "pointer disabled true without children", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Ptr, + Disabled: true, + }, + }}, + element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{}, + expected: expected{element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{}, + }, + }, + { + desc: "pointer disabled true with children", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Disabled: true, + Kind: reflect.Ptr, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "huu", Kind: reflect.String}, + {Name: "Fuu", FieldName: "Fuu", Value: "6", Kind: reflect.Int}, + }}, + }}, + element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{}, + expected: expected{element: &struct { + Foo *struct { + Fii string + Fuu int + } `label:"allowEmpty"` + }{}, + }, + }, + { + desc: "map string", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Map, + Children: []*Node{ + {Name: "name1", Value: "hii", Kind: reflect.String}, + {Name: "name2", Value: "huu", Kind: reflect.String}, + }}, + }}, + element: &struct { + Foo map[string]string + }{}, + expected: expected{element: &struct { + Foo map[string]string + }{ + Foo: map[string]string{ + "name1": "hii", + "name2": "huu", + }}, + }, + }, + { + desc: "map struct", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Map, + Children: []*Node{ + { + Name: "name1", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Kind: reflect.String, Value: "hii"}, + }, + }, + { + Name: "name2", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Kind: reflect.String, Value: "huu"}, + }, + }, + }}, + }}, + element: &struct { + Foo map[string]struct{ Fii string } + }{}, + expected: expected{element: &struct { + Foo map[string]struct{ Fii string } + }{ + Foo: map[string]struct{ Fii string }{ + "name1": {Fii: "hii"}, + "name2": {Fii: "huu"}, + }}, + }, + }, + { + desc: "slice string", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "huu,hii,hoo", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []string }{}, + expected: expected{element: &struct{ Foo []string }{Foo: []string{"huu", "hii", "hoo"}}}, + }, + { + desc: "empty slice", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []string }{}, + expected: expected{element: &struct{ Foo []string }{Foo: nil}}, + }, + { + desc: "slice int", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int }{}, + expected: expected{element: &struct{ Foo []int }{Foo: []int{4, 3, 6}}}, + }, + { + desc: "slice invalid int", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int }{}, + expected: expected{error: true}, + }, + { + desc: "slice int8", + node: &Node{ + Name: "traefik", + Kind: reflect.Slice, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int8 }{}, + expected: expected{element: &struct{ Foo []int8 }{Foo: []int8{4, 3, 6}}}, + }, + { + desc: "slice invalid int8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int8 }{}, + expected: expected{error: true}, + }, + { + desc: "slice int16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int16 }{}, + expected: expected{element: &struct{ Foo []int16 }{Foo: []int16{4, 3, 6}}}, + }, + { + desc: "slice invalid int16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int16 }{}, + expected: expected{error: true}, + }, + { + desc: "slice int32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int32 }{}, + expected: expected{element: &struct{ Foo []int32 }{Foo: []int32{4, 3, 6}}}, + }, + { + desc: "slice invalid int32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int32 }{}, + expected: expected{error: true}, + }, + { + desc: "slice int64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int64 }{}, + expected: expected{element: &struct{ Foo []int64 }{Foo: []int64{4, 3, 6}}}, + }, + { + desc: "slice invalid int64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []int64 }{}, + expected: expected{error: true}, + }, + { + desc: "slice uint", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint }{}, + expected: expected{element: &struct{ Foo []uint }{Foo: []uint{4, 3, 6}}}, + }, + { + desc: "slice invalid uint", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint }{}, + expected: expected{error: true}, + }, + { + desc: "slice uint8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint8 }{}, + expected: expected{element: &struct{ Foo []uint8 }{Foo: []uint8{4, 3, 6}}}, + }, + { + desc: "slice invalid uint8", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint8 }{}, + expected: expected{error: true}, + }, + { + desc: "slice uint16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint16 }{}, + expected: expected{element: &struct{ Foo []uint16 }{Foo: []uint16{4, 3, 6}}}, + }, + { + desc: "slice invalid uint16", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint16 }{}, + expected: expected{error: true}, + }, + { + desc: "slice uint32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint32 }{}, + expected: expected{element: &struct{ Foo []uint32 }{Foo: []uint32{4, 3, 6}}}, + }, + { + desc: "slice invalid uint32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint32 }{}, + expected: expected{error: true}, + }, + { + desc: "slice uint64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint64 }{}, + expected: expected{element: &struct{ Foo []uint64 }{Foo: []uint64{4, 3, 6}}}, + }, + { + desc: "slice invalid uint64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []uint64 }{}, + expected: expected{error: true}, + }, + { + desc: "slice float32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []float32 }{}, + expected: expected{element: &struct{ Foo []float32 }{Foo: []float32{4, 3, 6}}}, + }, + { + desc: "slice invalid float32", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []float32 }{}, + expected: expected{error: true}, + }, + { + desc: "slice float64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "4,3,6", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []float64 }{}, + expected: expected{element: &struct{ Foo []float64 }{Foo: []float64{4, 3, 6}}}, + }, + { + desc: "slice invalid float64", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "four,three,six", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []float64 }{}, + expected: expected{error: true}, + }, + { + desc: "slice bool", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "true, false, true", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []bool }{}, + expected: expected{element: &struct{ Foo []bool }{Foo: []bool{true, false, true}}}, + }, + { + desc: "slice invalid bool", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bool, false, true", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []bool }{}, + expected: expected{error: true}, + }, + { + desc: "slice struct", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "huu", Kind: reflect.Slice}, + }, + }, + element: &struct{ Foo []struct{ Fii string } }{}, + expected: expected{error: true}, + }, + { + desc: "slice slice-as-struct", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Fii", + FieldName: "Foo", + Kind: reflect.Slice, + Children: []*Node{ + {Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"}, + {Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"}, + }, + }, + }, + }, + element: &struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{}, + expected: expected{element: &struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{ + Foo: []struct { + Bar string + Bir string + }{ + { + Bar: "haa", + Bir: "hii", + }, + }, + }}, + }, + { + desc: "slice slice-as-struct without children", + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Fii", + FieldName: "Foo", + Kind: reflect.Slice, + }, + }, + }, + element: &struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{}, + expected: expected{error: true}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + err := Fill(test.element, test.node) + if test.expected.error { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, test.expected.element, test.element) + } + }) + } +} diff --git a/provider/label/internal/element_nodes.go b/provider/label/internal/element_nodes.go new file mode 100644 index 000000000..4436a00f8 --- /dev/null +++ b/provider/label/internal/element_nodes.go @@ -0,0 +1,163 @@ +package internal + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// EncodeToNode Converts an element to a node. +// element -> nodes +func EncodeToNode(element interface{}) (*Node, error) { + rValue := reflect.ValueOf(element) + node := &Node{Name: "traefik"} + + err := setNodeValue(node, rValue) + if err != nil { + return nil, err + } + + return node, nil +} + +func setNodeValue(node *Node, rValue reflect.Value) error { + switch rValue.Kind() { + case reflect.String: + node.Value = rValue.String() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + node.Value = strconv.FormatInt(rValue.Int(), 10) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + node.Value = strconv.FormatUint(rValue.Uint(), 10) + case reflect.Float32, reflect.Float64: + node.Value = strconv.FormatFloat(rValue.Float(), 'f', 6, 64) + case reflect.Bool: + node.Value = strconv.FormatBool(rValue.Bool()) + case reflect.Struct: + return setStructValue(node, rValue) + case reflect.Ptr: + return setNodeValue(node, rValue.Elem()) + case reflect.Map: + return setMapValue(node, rValue) + case reflect.Slice: + return setSliceValue(node, rValue) + default: + // noop + } + + return nil +} + +func setStructValue(node *Node, rValue reflect.Value) error { + rType := rValue.Type() + + for i := 0; i < rValue.NumField(); i++ { + field := rType.Field(i) + fieldValue := rValue.Field(i) + + if !isExported(field) { + continue + } + + if field.Tag.Get(TagLabel) == "-" { + continue + } + + if err := isSupportedType(field); err != nil { + return err + } + + if isSkippedField(field, fieldValue) { + continue + } + + nodeName := field.Name + if field.Type.Kind() == reflect.Slice && len(field.Tag.Get(TagLabelSliceAsStruct)) != 0 { + nodeName = field.Tag.Get(TagLabelSliceAsStruct) + } + + child := &Node{Name: nodeName, FieldName: field.Name} + + if err := setNodeValue(child, fieldValue); err != nil { + return err + } + + if field.Type.Kind() == reflect.Ptr && len(child.Children) == 0 { + if field.Tag.Get(TagLabel) != "allowEmpty" { + continue + } + + child.Value = "true" + } + + node.Children = append(node.Children, child) + } + + return nil +} + +func setMapValue(node *Node, rValue reflect.Value) error { + for _, key := range rValue.MapKeys() { + child := &Node{Name: key.String(), FieldName: key.String()} + node.Children = append(node.Children, child) + + if err := setNodeValue(child, rValue.MapIndex(key)); err != nil { + return err + } + } + return nil +} + +func setSliceValue(node *Node, rValue reflect.Value) error { + // label-slice-as-struct + if rValue.Type().Elem().Kind() == reflect.Struct && !strings.EqualFold(node.Name, node.FieldName) { + if rValue.Len() > 1 { + return fmt.Errorf("node %s has too many slice entries: %d", node.Name, rValue.Len()) + } + + if err := setNodeValue(node, rValue.Index(0)); err != nil { + return err + } + } + + var values []string + + for i := 0; i < rValue.Len(); i++ { + eValue := rValue.Index(i) + + switch eValue.Kind() { + case reflect.String: + values = append(values, eValue.String()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + values = append(values, strconv.FormatInt(eValue.Int(), 10)) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + values = append(values, strconv.FormatUint(eValue.Uint(), 10)) + case reflect.Float32, reflect.Float64: + values = append(values, strconv.FormatFloat(eValue.Float(), 'f', 6, 64)) + case reflect.Bool: + values = append(values, strconv.FormatBool(eValue.Bool())) + default: + // noop + } + } + + node.Value = strings.Join(values, ", ") + return nil +} + +func isSkippedField(field reflect.StructField, fieldValue reflect.Value) bool { + if field.Type.Kind() == reflect.String && fieldValue.Len() == 0 { + return true + } + + if field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct && fieldValue.IsNil() { + return true + } + + if (field.Type.Kind() == reflect.Slice || field.Type.Kind() == reflect.Map) && + (fieldValue.IsNil() || fieldValue.Len() == 0) { + return true + } + + return false +} diff --git a/provider/label/internal/element_nodes_test.go b/provider/label/internal/element_nodes_test.go new file mode 100644 index 000000000..4f811affb --- /dev/null +++ b/provider/label/internal/element_nodes_test.go @@ -0,0 +1,593 @@ +package internal + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncodeToNode(t *testing.T) { + type expected struct { + node *Node + error bool + } + + testCases := []struct { + desc string + element interface{} + expected expected + }{ + { + desc: "string", + element: struct { + Foo string + }{Foo: "bar"}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar"}, + }}, + }, + }, + { + desc: "2 string fields", + element: struct { + Foo string + Fii string + }{Foo: "bar", Fii: "hii"}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar"}, + {Name: "Fii", FieldName: "Fii", Value: "hii"}, + }}, + }, + }, + { + desc: "int", + element: struct { + Foo int + }{Foo: 1}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "1"}, + }}, + }, + }, + { + desc: "int8", + element: struct { + Foo int8 + }{Foo: 2}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2"}, + }}, + }, + }, + { + desc: "int16", + element: struct { + Foo int16 + }{Foo: 2}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2"}, + }}, + }, + }, + { + desc: "int32", + element: struct { + Foo int32 + }{Foo: 2}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2"}, + }}, + }, + }, + { + desc: "int64", + element: struct { + Foo int64 + }{Foo: 2}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2"}, + }}, + }, + }, + { + desc: "uint", + element: struct { + Foo uint + }{Foo: 1}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "1"}, + }}, + }, + }, + { + desc: "uint8", + element: struct { + Foo uint8 + }{Foo: 2}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2"}, + }}, + }, + }, + { + desc: "uint16", + element: struct { + Foo uint16 + }{Foo: 2}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2"}, + }}, + }, + }, + { + desc: "uint32", + element: struct { + Foo uint32 + }{Foo: 2}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2"}, + }}, + }, + }, + { + desc: "uint64", + element: struct { + Foo uint64 + }{Foo: 2}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "2"}, + }}, + }, + }, + { + desc: "float32", + element: struct { + Foo float32 + }{Foo: 1.12}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "1.120000"}, + }}, + }, + }, + { + desc: "float64", + element: struct { + Foo float64 + }{Foo: 1.12}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "1.120000"}, + }}, + }, + }, + { + desc: "bool", + element: struct { + Foo bool + }{Foo: true}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "true"}, + }}, + }, + }, + { + desc: "struct", + element: struct { + Foo struct { + Fii string + Fuu string + } + }{ + Foo: struct { + Fii string + Fuu string + }{ + Fii: "hii", + Fuu: "huu", + }, + }, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "hii"}, + {Name: "Fuu", FieldName: "Fuu", Value: "huu"}, + }}, + }}, + }, + }, + { + desc: "struct unexported field", + element: struct { + Foo struct { + Fii string + fuu string + } + }{ + Foo: struct { + Fii string + fuu string + }{ + Fii: "hii", + fuu: "huu", + }, + }, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "hii"}, + }}, + }}, + }, + }, + { + desc: "struct pointer", + element: struct { + Foo *struct { + Fii string + Fuu string + } + }{ + Foo: &struct { + Fii string + Fuu string + }{ + Fii: "hii", + Fuu: "huu", + }, + }, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "hii"}, + {Name: "Fuu", FieldName: "Fuu", Value: "huu"}, + }}, + }}, + }, + }, + { + desc: "string pointer", + element: struct { + Foo *struct { + Fii *string + Fuu string + } + }{ + Foo: &struct { + Fii *string + Fuu string + }{ + Fii: func(v string) *string { return &v }("hii"), + Fuu: "huu", + }, + }, + expected: expected{error: true}, + }, + { + desc: "struct nil pointer", + element: struct { + Foo *struct { + Fii *string + Fuu string + } + }{ + Foo: &struct { + Fii *string + Fuu string + }{ + Fuu: "huu", + }, + }, + expected: expected{error: true}, + }, + { + desc: "struct nil struct pointer", + element: struct { + Foo *struct { + Fii *string + Fuu string + } + }{ + Foo: nil, + }, + expected: expected{node: &Node{Name: "traefik"}}, + }, + { + desc: "struct pointer, not allowEmpty", + element: struct { + Foo *struct { + Fii string + Fuu string + } + }{ + Foo: &struct { + Fii string + Fuu string + }{}, + }, + expected: expected{node: &Node{Name: "traefik"}}, + }, + { + desc: "struct pointer, allowEmpty", + element: struct { + Foo *struct { + Fii string + Fuu string + } `label:"allowEmpty"` + }{ + Foo: &struct { + Fii string + Fuu string + }{}, + }, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "true"}, + }}, + }, + }, + { + desc: "map", + element: struct { + Foo struct { + Bar map[string]string + } + }{ + Foo: struct { + Bar map[string]string + }{ + Bar: map[string]string{ + "name1": "huu", + }, + }, + }, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Children: []*Node{ + {Name: "name1", FieldName: "name1", Value: "huu"}, + }}, + }}, + }}}, + }, + { + desc: "empty map", + element: struct { + Bar map[string]string + }{ + Bar: map[string]string{}, + }, + expected: expected{node: &Node{Name: "traefik"}}, + }, + { + desc: "map nil", + element: struct { + Bar map[string]string + }{ + Bar: nil, + }, + expected: expected{node: &Node{Name: "traefik"}}, + }, + { + desc: "map with non string key", + element: struct { + Foo struct { + Bar map[int]string + } + }{ + Foo: struct { + Bar map[int]string + }{ + Bar: map[int]string{ + 1: "huu", + }, + }, + }, + expected: expected{error: true}, + }, + { + desc: "slice of string", + element: struct{ Bar []string }{Bar: []string{"huu", "hii"}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "huu, hii"}, + }}, + }, + }, + { + desc: "slice of int", + element: struct{ Bar []int }{Bar: []int{4, 2, 3}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, + }}, + }, + }, + { + desc: "slice of int8", + element: struct{ Bar []int8 }{Bar: []int8{4, 2, 3}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, + }}, + }, + }, + { + desc: "slice of int16", + element: struct{ Bar []int16 }{Bar: []int16{4, 2, 3}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, + }}, + }, + }, + { + desc: "slice of int32", + element: struct{ Bar []int32 }{Bar: []int32{4, 2, 3}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, + }}, + }, + }, + { + desc: "slice of int64", + element: struct{ Bar []int64 }{Bar: []int64{4, 2, 3}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, + }}, + }, + }, + { + desc: "slice of uint", + element: struct{ Bar []uint }{Bar: []uint{4, 2, 3}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, + }}, + }, + }, + { + desc: "slice of uint8", + element: struct{ Bar []uint8 }{Bar: []uint8{4, 2, 3}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, + }}, + }, + }, + { + desc: "slice of uint16", + element: struct{ Bar []uint16 }{Bar: []uint16{4, 2, 3}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, + }}, + }, + }, + { + desc: "slice of uint32", + element: struct{ Bar []uint32 }{Bar: []uint32{4, 2, 3}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, + }}, + }, + }, + { + desc: "slice of uint64", + element: struct{ Bar []uint64 }{Bar: []uint64{4, 2, 3}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4, 2, 3"}, + }}, + }, + }, + { + desc: "slice of float32", + element: struct{ Bar []float32 }{Bar: []float32{4.1, 2, 3.2}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4.100000, 2.000000, 3.200000"}, + }}, + }, + }, + { + desc: "slice of float64", + element: struct{ Bar []float64 }{Bar: []float64{4.1, 2, 3.2}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "4.100000, 2.000000, 3.200000"}, + }}, + }, + }, + { + desc: "slice of bool", + element: struct{ Bar []bool }{Bar: []bool{true, false, true}}, + expected: expected{node: &Node{Name: "traefik", Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "true, false, true"}, + }}, + }, + }, + { + desc: "slice label-slice-as-struct", + element: &struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{ + Foo: []struct { + Bar string + Bir string + }{ + { + Bar: "haa", + Bir: "hii", + }, + }, + }, + expected: expected{node: &Node{ + Name: "traefik", + Children: []*Node{{ + Name: "Fii", + FieldName: "Foo", + Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "haa"}, + {Name: "Bir", FieldName: "Bir", Value: "hii"}, + }, + }}, + }}, + }, + { + desc: "slice label-slice-as-struct several slice entries", + element: &struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{ + Foo: []struct { + Bar string + Bir string + }{ + { + Bar: "haa", + Bir: "hii", + }, + { + Bar: "haa", + Bir: "hii", + }, + }, + }, + expected: expected{error: true}, + }, + { + desc: "empty slice", + element: struct { + Bar []string + }{ + Bar: []string{}, + }, + expected: expected{node: &Node{Name: "traefik"}}, + }, + { + desc: "nil slice", + element: struct { + Bar []string + }{ + Bar: nil, + }, + expected: expected{node: &Node{Name: "traefik"}}, + }, + { + desc: "ignore slice", + element: struct { + Bar []string `label:"-"` + }{ + Bar: []string{"huu", "hii"}, + }, + expected: expected{node: &Node{Name: "traefik"}}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + node, err := EncodeToNode(test.element) + + if test.expected.error { + require.Error(t, err) + } else { + require.NoError(t, err) + + assert.Equal(t, test.expected.node, node) + } + }) + } +} diff --git a/provider/label/internal/labels_decode.go b/provider/label/internal/labels_decode.go new file mode 100644 index 000000000..540e5961a --- /dev/null +++ b/provider/label/internal/labels_decode.go @@ -0,0 +1,68 @@ +package internal + +import ( + "fmt" + "sort" + "strings" +) + +// DecodeToNode Converts the labels to a node. +// labels -> nodes +func DecodeToNode(labels map[string]string) (*Node, error) { + var sortedKeys []string + for key := range labels { + sortedKeys = append(sortedKeys, key) + } + sort.Strings(sortedKeys) + + labelRoot := "traefik" + + var node *Node + for i, key := range sortedKeys { + split := strings.Split(key, ".") + + if split[0] != labelRoot { + // TODO (@ldez): error or continue + return nil, fmt.Errorf("invalid label root %s", split[0]) + } + + labelRoot = split[0] + + if i == 0 { + node = &Node{} + } + decodeToNode(node, split, labels[key]) + } + + return node, nil +} + +func decodeToNode(root *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 := &Node{Name: path[1]} + decodeToNode(child, path[1:], value) + root.Children = append(root.Children, child) + } + } else { + root.Value = value + } +} + +func containsNode(nodes []*Node, name string) *Node { + for _, n := range nodes { + if name == n.Name { + return n + } + } + return nil +} diff --git a/provider/label/internal/labels_decode_test.go b/provider/label/internal/labels_decode_test.go new file mode 100644 index 000000000..2d60610a4 --- /dev/null +++ b/provider/label/internal/labels_decode_test.go @@ -0,0 +1,190 @@ +package internal + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDecodeToNode(t *testing.T) { + type expected struct { + error bool + node *Node + } + + testCases := []struct { + desc string + in map[string]string + expected expected + }{ + { + desc: "level 0", + in: map[string]string{"traefik": "bar"}, + expected: expected{node: &Node{ + Name: "traefik", + Value: "bar", + }}, + }, + { + desc: "level 1", + in: map[string]string{ + "traefik.foo": "bar", + }, + expected: expected{node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Value: "bar"}, + }, + }}, + }, + { + desc: "level 1 empty value", + in: map[string]string{ + "traefik.foo": "", + }, + expected: expected{node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Value: ""}, + }, + }}, + }, + { + desc: "level 2", + in: map[string]string{ + "traefik.foo.bar": "bar", + }, + expected: expected{node: &Node{ + Name: "traefik", + Children: []*Node{{ + Name: "foo", + Children: []*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, level 1", + in: map[string]string{ + "traefik.foo": "bar", + "traefik.fii": "bur", + }, + expected: expected{node: &Node{ + Name: "traefik", + Children: []*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: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Children: []*Node{ + {Name: "aaa", Value: "bar"}, + {Name: "bbb", Value: "bur"}, + }}, + }, + }}, + }, + { + 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: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Children: []*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: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Children: []*Node{ + {Name: "bar", Children: []*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: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "bar", Children: []*Node{ + {Name: "foo", Children: []*Node{ + {Name: "bbb", Value: "bir"}, + }}, + }}, + {Name: "foo", Children: []*Node{ + {Name: "bar", Children: []*Node{ + {Name: "aaa", Value: "bar"}, + {Name: "bbb", Value: "bur"}, + }}, + }}, + }, + }}, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + out, err := DecodeToNode(test.in) + + 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)) + } + } + }) + } +} diff --git a/provider/label/internal/labels_encode.go b/provider/label/internal/labels_encode.go new file mode 100644 index 000000000..c55cf130c --- /dev/null +++ b/provider/label/internal/labels_encode.go @@ -0,0 +1,24 @@ +package internal + +// EncodeNode Converts a node to labels. +// nodes -> labels +func EncodeNode(node *Node) map[string]string { + labels := make(map[string]string) + encodeNode(labels, node.Name, node) + return labels +} + +func encodeNode(labels map[string]string, root string, node *Node) { + for _, child := range node.Children { + if child.Disabled { + continue + } + + childName := root + "." + child.Name + if len(child.Children) > 0 { + encodeNode(labels, childName, child) + } else if len(child.Name) > 0 { + labels[childName] = child.Value + } + } +} diff --git a/provider/label/internal/labels_encode_test.go b/provider/label/internal/labels_encode_test.go new file mode 100644 index 000000000..6961c808b --- /dev/null +++ b/provider/label/internal/labels_encode_test.go @@ -0,0 +1,156 @@ +package internal + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEncodeNode(t *testing.T) { + testCases := []struct { + desc string + node *Node + expected map[string]string + }{ + { + desc: "1 label", + node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "aaa", Value: "bar"}, + }, + }, + expected: map[string]string{ + "traefik.aaa": "bar", + }, + }, + { + desc: "2 labels", + node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "aaa", Value: "bar"}, + {Name: "bbb", Value: "bur"}, + }, + }, + expected: map[string]string{ + "traefik.aaa": "bar", + "traefik.bbb": "bur", + }, + }, + { + desc: "2 labels, 1 disabled", + node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "aaa", Value: "bar"}, + {Name: "bbb", Value: "bur", Disabled: true}, + }, + }, + expected: map[string]string{ + "traefik.aaa": "bar", + }, + }, + { + desc: "2 levels", + node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Children: []*Node{ + {Name: "aaa", Value: "bar"}, + }}, + }, + }, + expected: map[string]string{ + "traefik.foo.aaa": "bar", + }, + }, + { + desc: "3 levels", + node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Children: []*Node{ + {Name: "bar", Children: []*Node{ + {Name: "aaa", Value: "bar"}, + }}, + }}, + }, + }, + expected: map[string]string{ + "traefik.foo.bar.aaa": "bar", + }, + }, + { + desc: "2 levels, same root", + node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Children: []*Node{ + {Name: "bar", Children: []*Node{ + {Name: "aaa", Value: "bar"}, + {Name: "bbb", Value: "bur"}, + }}, + }}, + }, + }, + expected: map[string]string{ + "traefik.foo.bar.aaa": "bar", + "traefik.foo.bar.bbb": "bur", + }, + }, + { + desc: "several levels, different root", + node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "bar", Children: []*Node{ + {Name: "ccc", Value: "bir"}, + }}, + {Name: "foo", Children: []*Node{ + {Name: "bar", Children: []*Node{ + {Name: "aaa", Value: "bar"}, + }}, + }}, + }, + }, + expected: map[string]string{ + "traefik.foo.bar.aaa": "bar", + "traefik.bar.ccc": "bir", + }, + }, + { + desc: "multiple labels, multiple levels", + node: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "bar", Children: []*Node{ + {Name: "ccc", Value: "bir"}, + }}, + {Name: "foo", Children: []*Node{ + {Name: "bar", Children: []*Node{ + {Name: "aaa", Value: "bar"}, + {Name: "bbb", Value: "bur"}, + }}, + }}, + }, + }, + expected: map[string]string{ + "traefik.foo.bar.aaa": "bar", + "traefik.foo.bar.bbb": "bur", + "traefik.bar.ccc": "bir", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + labels := EncodeNode(test.node) + + assert.Equal(t, test.expected, labels) + }) + } +} diff --git a/provider/label/internal/node.go b/provider/label/internal/node.go new file mode 100644 index 000000000..fab2473a1 --- /dev/null +++ b/provider/label/internal/node.go @@ -0,0 +1,13 @@ +package internal + +import "reflect" + +// Node a label node. +type Node struct { + Name string `json:"name"` + FieldName string `json:"fieldName"` + Value string `json:"value,omitempty"` + Disabled bool `json:"disabled,omitempty"` + Kind reflect.Kind `json:"kind,omitempty"` + Children []*Node `json:"children,omitempty"` +} diff --git a/provider/label/internal/nodes_metadata.go b/provider/label/internal/nodes_metadata.go new file mode 100644 index 000000000..6556c660b --- /dev/null +++ b/provider/label/internal/nodes_metadata.go @@ -0,0 +1,164 @@ +package internal + +import ( + "errors" + "fmt" + "reflect" + "strings" +) + +// AddMetadata Adds metadata to a node. +// nodes + element -> nodes +func AddMetadata(structure interface{}, node *Node) error { + if len(node.Children) == 0 { + return fmt.Errorf("invalid node %s: no child", node.Name) + } + + if structure == nil { + return errors.New("nil structure") + } + + rootType := reflect.TypeOf(structure) + node.Kind = rootType.Kind() + + return browseChildren(rootType, node) +} + +func addMetadata(rootType reflect.Type, node *Node) error { + rType := rootType + if rootType.Kind() == reflect.Ptr { + rType = rootType.Elem() + } + + field, err := findTypedField(rType, node) + if err != nil { + return err + } + + if err = isSupportedType(field); err != nil { + return err + } + + fType := field.Type + node.Kind = fType.Kind() + + 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) != "allowEmpty" { + return fmt.Errorf("node %s (type %s) must have children", node.Name, fType) + } + + node.Disabled = len(node.Value) > 0 && node.Value != "true" && field.Tag.Get(TagLabel) == "allowEmpty" + } + + if len(node.Children) == 0 { + return nil + } + + if fType.Kind() == reflect.Struct || fType.Kind() == reflect.Ptr && fType.Elem().Kind() == reflect.Struct { + return browseChildren(fType, node) + } + + if fType.Kind() == reflect.Map { + for _, child := range node.Children { + // elem is a map entry value type + elem := fType.Elem() + child.Kind = elem.Kind() + + 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 { + return err + } + } + } + return nil + } + + // only for struct/Ptr with label-slice-as-struct tag + if fType.Kind() == reflect.Slice { + return browseChildren(fType.Elem(), node) + } + + return fmt.Errorf("invalid node %s: %v", node.Name, fType.Kind()) +} + +func 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 { + fieldName = cField.Name + } + + if isExported(cField) && strings.EqualFold(fieldName, node.Name) { + node.FieldName = cField.Name + return cField, nil + } + } + + return reflect.StructField{}, fmt.Errorf("field not found, node: %s", node.Name) +} + +func browseChildren(fType reflect.Type, node *Node) error { + for _, child := range node.Children { + if err := addMetadata(fType, child); err != nil { + return err + } + } + return nil +} + +// isExported return true is a struct field is exported, else false +// https://golang.org/pkg/reflect/#StructField +func isExported(f reflect.StructField) bool { + if f.PkgPath != "" && !f.Anonymous { + return false + } + return true +} + +func isSupportedType(field reflect.StructField) error { + fType := field.Type + + if fType.Kind() == reflect.Slice { + switch fType.Elem().Kind() { + case reflect.String, + reflect.Bool, + reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64, + reflect.Uintptr, + reflect.Float32, + reflect.Float64: + return nil + default: + if len(field.Tag.Get(TagLabelSliceAsStruct)) > 0 { + return nil + } + return fmt.Errorf("unsupported slice type: %v", fType) + } + } + + if fType.Kind() == reflect.Ptr && fType.Elem().Kind() != reflect.Struct { + return fmt.Errorf("unsupported pointer type: %v", fType.Elem()) + } + + if fType.Kind() == reflect.Map && fType.Key().Kind() != reflect.String { + return fmt.Errorf("unsupported map key type: %v", fType.Key()) + } + + if fType.Kind() == reflect.Func { + return fmt.Errorf("unsupported type: %v", fType) + } + + return nil +} diff --git a/provider/label/internal/nodes_metadata_test.go b/provider/label/internal/nodes_metadata_test.go new file mode 100644 index 000000000..891cc7f14 --- /dev/null +++ b/provider/label/internal/nodes_metadata_test.go @@ -0,0 +1,769 @@ +package internal + +import ( + "encoding/json" + "fmt" + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddMetadata(t *testing.T) { + type expected struct { + node *Node + error bool + } + + type interf interface{} + + testCases := []struct { + desc string + tree *Node + structure interface{} + expected expected + }{ + { + desc: "Empty Node", + tree: &Node{}, + structure: nil, + expected: expected{error: true}, + }, + { + desc: "Nil structure", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "bar"}, + }, + }, + structure: nil, + expected: expected{error: true}, + }, + { + desc: "level 0", + tree: &Node{Name: "traefik", Value: "bar"}, + expected: expected{error: true}, + }, + { + desc: "level 1", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar"}, + }, + }, + structure: struct{ Foo string }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, + }, + }, + }, + }, + { + desc: "level 1, pointer", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "bar"}, + }, + }, + structure: &struct{ Foo string }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Ptr, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, + }, + }, + }, + }, + { + desc: "level 1, slice", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "bar,bur"}, + }, + }, + structure: struct{ Foo []string }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar,bur", Kind: reflect.Slice}, + }, + }, + }, + }, + { + desc: "level 1, interface", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "", Children: []*Node{ + {Name: "Fii", Value: "hii"}, + }}, + }, + }, + structure: struct{ Foo interf }{}, + expected: expected{error: true}, + }, + { + desc: "level 1, slice struct", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "1,2"}, + }, + }, + structure: struct { + Foo []struct{ Foo string } + }{}, + expected: expected{error: true}, + }, + { + desc: "level 1, map string", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "name1", Value: "bar"}, + {Name: "name2", Value: "bur"}, + }}, + }, + }, + structure: struct{ Foo map[string]string }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Map, Children: []*Node{ + {Name: "name1", Value: "bar", Kind: reflect.String}, + {Name: "name2", Value: "bur", Kind: reflect.String}, + }}, + }, + }, + }, + }, + { + desc: "level 1, map struct", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "name1", Children: []*Node{ + {Name: "Fii", Value: "bar"}, + }}, + }}, + }, + }, + structure: struct { + Foo map[string]struct{ Fii string } + }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Map, Children: []*Node{ + {Name: "name1", Kind: reflect.Struct, Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Value: "bar", Kind: reflect.String}, + }}, + }}, + }, + }, + }, + }, + { + desc: "level 1, map int as key", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "name1", Children: []*Node{ + {Name: "Fii", Value: "bar"}, + }}, + }}, + }, + }, + structure: struct { + Foo map[int]struct{ Fii string } + }{}, + expected: expected{error: true}, + }, + { + desc: "level 1, int pointer", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "0"}, + }, + }, + structure: struct { + Foo *int + }{}, + expected: expected{error: true}, + }, + { + desc: "level 1, 2 children with different types", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "bar"}, + {Name: "Fii", Value: "1"}, + }, + }, + structure: struct { + Foo string + Fii int + }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, + {Name: "Fii", FieldName: "Fii", Value: "1", Kind: reflect.Int}, + }, + }, + }, + }, + { + desc: "level 1, use exported instead of unexported", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Value: "bar"}, + }, + }, + structure: struct { + foo int + Foo string + }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "foo", Value: "bar", FieldName: "Foo", Kind: reflect.String}, + }, + }, + }, + }, + { + desc: "level 1, unexported", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "foo", Value: "bar"}, + }, + }, + structure: struct { + foo string + }{}, + expected: expected{error: true}, + }, + { + desc: "level 1, 3 children with different types", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "bar"}, + {Name: "Fii", Value: "1"}, + {Name: "Fuu", Value: "true"}, + }, + }, + structure: struct { + Foo string + Fii int + Fuu bool + }{}, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "bar", Kind: reflect.String}, + {Name: "Fii", FieldName: "Fii", Value: "1", Kind: reflect.Int}, + {Name: "Fuu", FieldName: "Fuu", Value: "true", Kind: reflect.Bool}, + }, + }, + }, + }, + { + desc: "level 2", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "Bar", Value: "bir"}, + }}, + }, + }, + structure: struct { + Foo struct { + Bar string + } + }{ + Foo: struct { + Bar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Struct, Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "bir", Kind: reflect.String}, + }}, + }, + }, + }, + }, + { + desc: "level 2, struct without children", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo"}, + }, + }, + structure: struct { + Foo struct { + Bar string + } + }{ + Foo: struct { + Bar string + }{}, + }, + expected: expected{error: true}, + }, + { + desc: "level 2, slice-as-struct", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Fii", Children: []*Node{ + {Name: "bar", Value: "haa"}, + {Name: "bir", Value: "hii"}, + }}, + }, + }, + structure: struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{ + Foo: []struct { + Bar string + Bir string + }{}, + }, + expected: expected{node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Fii", + FieldName: "Foo", + Kind: reflect.Slice, + Children: []*Node{ + {Name: "bar", FieldName: "Bar", Kind: reflect.String, Value: "haa"}, + {Name: "bir", FieldName: "Bir", Kind: reflect.String, Value: "hii"}, + }, + }, + }, + }}, + }, + { + desc: "level 2, slice-as-struct without children", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Fii"}, + }, + }, + structure: struct { + Foo []struct { + Bar string + Bir string + } `label-slice-as-struct:"Fii"` + }{ + Foo: []struct { + Bar string + Bir string + }{}, + }, + expected: expected{node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Fii", + FieldName: "Foo", + Kind: reflect.Slice, + }, + }, + }}, + }, + { + desc: "level 2, struct with allowEmpty, value true", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "true"}, + }, + }, + structure: struct { + Foo struct { + Bar string + } `label:"allowEmpty"` + }{ + Foo: struct { + Bar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "true", Kind: reflect.Struct}, + }, + }, + }, + }, + { + desc: "level 2, struct with allowEmpty, value false", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "false"}, + }, + }, + structure: struct { + Foo struct { + Bar string + } `label:"allowEmpty"` + }{ + Foo: struct { + Bar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Value: "false", Disabled: true, Kind: reflect.Struct}, + }, + }, + }, + }, + { + desc: "level 2, struct with allowEmpty with children, value false", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Value: "false", Children: []*Node{ + {Name: "Bar", Value: "hii"}, + }}, + }, + }, + structure: struct { + Foo struct { + Bar string + } `label:"allowEmpty"` + }{ + Foo: struct { + Bar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Value: "false", + Disabled: true, + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "hii", Kind: reflect.String}, + }, + }, + }, + }, + }, + }, + { + desc: "level 2, struct pointer without children", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo"}, + }, + }, + structure: struct { + Foo *struct { + Bar string + } + }{ + Foo: &struct { + Bar string + }{}, + }, + expected: expected{error: true}, + }, + { + desc: "level 2, map without children", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo"}, + }, + }, + structure: struct { + Foo map[string]string + }{ + Foo: map[string]string{}, + }, + expected: expected{error: true}, + }, + { + desc: "level 2, pointer", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "Bar", Value: "bir"}, + }}, + }, + }, + structure: struct { + Foo *struct { + Bar string + } + }{ + Foo: &struct { + Bar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Foo", FieldName: "Foo", Kind: reflect.Ptr, Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "bir", Kind: reflect.String}, + }}, + }, + }, + }, + }, + { + desc: "level 2, 2 children", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "Bar", Value: "bir"}, + {Name: "Bur", Value: "fuu"}, + }}, + }, + }, + structure: struct { + Foo struct { + Bar string + Bur string + } + }{ + Foo: struct { + Bar string + Bur string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Bar", FieldName: "Bar", Value: "bir", Kind: reflect.String}, + {Name: "Bur", FieldName: "Bur", Value: "fuu", Kind: reflect.String}, + }}, + }, + }, + }, + }, + { + desc: "level 3", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "Bar", Children: []*Node{ + {Name: "Bur", Value: "fuu"}, + }}, + }}, + }, + }, + structure: struct { + Foo struct { + Bar struct { + Bur string + } + } + }{ + Foo: struct { + Bar struct { + Bur string + } + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Bar", + FieldName: "Bar", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Bur", FieldName: "Bur", Value: "fuu", Kind: reflect.String}, + }}, + }}, + }, + }, + }, + }, + { + desc: "level 3, 2 children level 1, 2 children level 2, 2 children level 3", + tree: &Node{ + Name: "traefik", + Children: []*Node{ + {Name: "Foo", Children: []*Node{ + {Name: "Bar", Children: []*Node{ + {Name: "Fii", Value: "fii"}, + {Name: "Fee", Value: "1"}, + }}, + {Name: "Bur", Children: []*Node{ + {Name: "Faa", Value: "faa"}, + }}, + }}, + {Name: "Fii", Children: []*Node{ + {Name: "FiiBar", Value: "fiiBar"}, + }}, + }, + }, + structure: struct { + Foo struct { + Bar struct { + Fii string + Fee int + } + Bur struct { + Faa string + } + } + Fii struct { + FiiBar string + } + }{ + Foo: struct { + Bar struct { + Fii string + Fee int + } + Bur struct { + Faa string + } + }{}, + Fii: struct { + FiiBar string + }{}, + }, + expected: expected{ + node: &Node{ + Name: "traefik", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Foo", + FieldName: "Foo", + Kind: reflect.Struct, + Children: []*Node{ + { + Name: "Bar", + FieldName: "Bar", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Fii", FieldName: "Fii", Kind: reflect.String, Value: "fii"}, + {Name: "Fee", FieldName: "Fee", Kind: reflect.Int, Value: "1"}, + }}, + { + Name: "Bur", + FieldName: "Bur", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "Faa", FieldName: "Faa", Kind: reflect.String, Value: "faa"}, + }}, + }}, + { + Name: "Fii", + FieldName: "Fii", + Kind: reflect.Struct, + Children: []*Node{ + {Name: "FiiBar", FieldName: "FiiBar", Kind: reflect.String, Value: "fiiBar"}, + }}, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + err := AddMetadata(test.structure, test.tree) + + if test.expected.error { + assert.Error(t, err) + } else { + require.NoError(t, err) + + if !assert.Equal(t, test.expected.node, test.tree) { + bytes, errM := json.MarshalIndent(test.tree, "", " ") + require.NoError(t, errM) + fmt.Println(string(bytes)) + } + } + }) + } +} diff --git a/provider/label/internal/tags.go b/provider/label/internal/tags.go new file mode 100644 index 000000000..43d232375 --- /dev/null +++ b/provider/label/internal/tags.go @@ -0,0 +1,12 @@ +package internal + +const ( + // TagLabel allow to apply a custom behavior. + // - "allowEmpty": allow to create an empty struct. + // - "-": ignore the field. + TagLabel = "label" + + // TagLabelSliceAsStruct allow to use a slice of struct by creating one entry into the slice. + // The value is the substitution name use in the label to access the slice. + TagLabelSliceAsStruct = "label-slice-as-struct" +) diff --git a/provider/label/parser.go b/provider/label/parser.go new file mode 100644 index 000000000..e531cd5a6 --- /dev/null +++ b/provider/label/parser.go @@ -0,0 +1,39 @@ +package label + +import ( + "github.com/containous/traefik/config" + "github.com/containous/traefik/provider/label/internal" +) + +// Decode Converts the labels to a configuration. +// labels -> [ node -> node + metadata (type) ] -> element (node) +func Decode(labels map[string]string) (*config.Configuration, error) { + node, err := internal.DecodeToNode(labels) + if err != nil { + return nil, err + } + + conf := &config.Configuration{} + err = internal.AddMetadata(conf, node) + if err != nil { + return nil, err + } + + err = internal.Fill(conf, node) + if err != nil { + return nil, err + } + + return conf, nil +} + +// Encode Converts a configuration to labels. +// element -> node (value) -> label (node) +func Encode(conf *config.Configuration) (map[string]string, error) { + node, err := internal.EncodeToNode(conf) + if err != nil { + return nil, err + } + + return internal.EncodeNode(node), nil +} diff --git a/provider/label/parser_test.go b/provider/label/parser_test.go new file mode 100644 index 000000000..8c328ca4c --- /dev/null +++ b/provider/label/parser_test.go @@ -0,0 +1,913 @@ +package label + +import ( + "fmt" + "testing" + "time" + + "github.com/containous/flaeg/parse" + "github.com/containous/traefik/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDecode(t *testing.T) { + labels := map[string]string{ + "traefik.middlewares.Middleware0.addprefix.prefix": "foobar", + "traefik.middlewares.Middleware1.basicauth.headerfield": "foobar", + "traefik.middlewares.Middleware1.basicauth.realm": "foobar", + "traefik.middlewares.Middleware1.basicauth.removeheader": "true", + "traefik.middlewares.Middleware1.basicauth.users": "foobar, fiibar", + "traefik.middlewares.Middleware1.basicauth.usersfile": "foobar", + "traefik.middlewares.Middleware2.buffering.maxrequestbodybytes": "42", + "traefik.middlewares.Middleware2.buffering.maxresponsebodybytes": "42", + "traefik.middlewares.Middleware2.buffering.memrequestbodybytes": "42", + "traefik.middlewares.Middleware2.buffering.memresponsebodybytes": "42", + "traefik.middlewares.Middleware2.buffering.retryexpression": "foobar", + "traefik.middlewares.Middleware3.chain.middlewares": "foobar, fiibar", + "traefik.middlewares.Middleware4.circuitbreaker.expression": "foobar", + "traefik.middlewares.Middleware5.digestauth.headerfield": "foobar", + "traefik.middlewares.Middleware5.digestauth.realm": "foobar", + "traefik.middlewares.Middleware5.digestauth.removeheader": "true", + "traefik.middlewares.Middleware5.digestauth.users": "foobar, fiibar", + "traefik.middlewares.Middleware5.digestauth.usersfile": "foobar", + "traefik.middlewares.Middleware6.errors.query": "foobar", + "traefik.middlewares.Middleware6.errors.service": "foobar", + "traefik.middlewares.Middleware6.errors.status": "foobar, fiibar", + "traefik.middlewares.Middleware7.forwardauth.address": "foobar", + "traefik.middlewares.Middleware7.forwardauth.authresponseheaders": "foobar, fiibar", + "traefik.middlewares.Middleware7.forwardauth.tls.ca": "foobar", + "traefik.middlewares.Middleware7.forwardauth.tls.caoptional": "true", + "traefik.middlewares.Middleware7.forwardauth.tls.cert": "foobar", + "traefik.middlewares.Middleware7.forwardauth.tls.insecureskipverify": "true", + "traefik.middlewares.Middleware7.forwardauth.tls.key": "foobar", + "traefik.middlewares.Middleware7.forwardauth.trustforwardheader": "true", + "traefik.middlewares.Middleware8.headers.allowedhosts": "foobar, fiibar", + "traefik.middlewares.Middleware8.headers.browserxssfilter": "true", + "traefik.middlewares.Middleware8.headers.contentsecuritypolicy": "foobar", + "traefik.middlewares.Middleware8.headers.contenttypenosniff": "true", + "traefik.middlewares.Middleware8.headers.custombrowserxssvalue": "foobar", + "traefik.middlewares.Middleware8.headers.customframeoptionsvalue": "foobar", + "traefik.middlewares.Middleware8.headers.customrequestheaders.name0": "foobar", + "traefik.middlewares.Middleware8.headers.customrequestheaders.name1": "foobar", + "traefik.middlewares.Middleware8.headers.customresponseheaders.name0": "foobar", + "traefik.middlewares.Middleware8.headers.customresponseheaders.name1": "foobar", + "traefik.middlewares.Middleware8.headers.forcestsheader": "true", + "traefik.middlewares.Middleware8.headers.framedeny": "true", + "traefik.middlewares.Middleware8.headers.hostsproxyheaders": "foobar, fiibar", + "traefik.middlewares.Middleware8.headers.isdevelopment": "true", + "traefik.middlewares.Middleware8.headers.publickey": "foobar", + "traefik.middlewares.Middleware8.headers.referrerpolicy": "foobar", + "traefik.middlewares.Middleware8.headers.sslforcehost": "true", + "traefik.middlewares.Middleware8.headers.sslhost": "foobar", + "traefik.middlewares.Middleware8.headers.sslproxyheaders.name0": "foobar", + "traefik.middlewares.Middleware8.headers.sslproxyheaders.name1": "foobar", + "traefik.middlewares.Middleware8.headers.sslredirect": "true", + "traefik.middlewares.Middleware8.headers.ssltemporaryredirect": "true", + "traefik.middlewares.Middleware8.headers.stsincludesubdomains": "true", + "traefik.middlewares.Middleware8.headers.stspreload": "true", + "traefik.middlewares.Middleware8.headers.stsseconds": "42", + "traefik.middlewares.Middleware9.ipwhitelist.ipstrategy.depth": "42", + "traefik.middlewares.Middleware9.ipwhitelist.ipstrategy.excludedips": "foobar, fiibar", + "traefik.middlewares.Middleware9.ipwhitelist.sourcerange": "foobar, fiibar", + "traefik.middlewares.Middleware10.maxconn.amount": "42", + "traefik.middlewares.Middleware10.maxconn.extractorfunc": "foobar", + "traefik.middlewares.Middleware11.passtlsclientcert.infos.notafter": "true", + "traefik.middlewares.Middleware11.passtlsclientcert.infos.notbefore": "true", + "traefik.middlewares.Middleware11.passtlsclientcert.infos.sans": "true", + "traefik.middlewares.Middleware11.passtlsclientcert.infos.subject.commonname": "true", + "traefik.middlewares.Middleware11.passtlsclientcert.infos.subject.country": "true", + "traefik.middlewares.Middleware11.passtlsclientcert.infos.subject.locality": "true", + "traefik.middlewares.Middleware11.passtlsclientcert.infos.subject.organization": "true", + "traefik.middlewares.Middleware11.passtlsclientcert.infos.subject.province": "true", + "traefik.middlewares.Middleware11.passtlsclientcert.infos.subject.serialnumber": "true", + "traefik.middlewares.Middleware11.passtlsclientcert.pem": "true", + "traefik.middlewares.Middleware12.ratelimit.extractorfunc": "foobar", + "traefik.middlewares.Middleware12.ratelimit.rateset.Rate0.average": "42", + "traefik.middlewares.Middleware12.ratelimit.rateset.Rate0.burst": "42", + "traefik.middlewares.Middleware12.ratelimit.rateset.Rate0.period": "42", + "traefik.middlewares.Middleware12.ratelimit.rateset.Rate1.average": "42", + "traefik.middlewares.Middleware12.ratelimit.rateset.Rate1.burst": "42", + "traefik.middlewares.Middleware12.ratelimit.rateset.Rate1.period": "42", + "traefik.middlewares.Middleware13.redirect.permanent": "true", + "traefik.middlewares.Middleware13.redirect.regex": "foobar", + "traefik.middlewares.Middleware13.redirect.replacement": "foobar", + "traefik.middlewares.Middleware14.replacepath.path": "foobar", + "traefik.middlewares.Middleware15.replacepathregex.regex": "foobar", + "traefik.middlewares.Middleware15.replacepathregex.replacement": "foobar", + "traefik.middlewares.Middleware16.retry.attempts": "42", + "traefik.middlewares.Middleware17.stripprefix.prefixes": "foobar, fiibar", + "traefik.middlewares.Middleware18.stripprefixregex.regex": "foobar, fiibar", + "traefik.middlewares.Middleware19.compress": "true", + + "traefik.routers.Router0.entrypoints": "foobar, fiibar", + "traefik.routers.Router0.middlewares": "foobar, fiibar", + "traefik.routers.Router0.priority": "42", + "traefik.routers.Router0.rule": "foobar", + "traefik.routers.Router0.service": "foobar", + "traefik.routers.Router1.entrypoints": "foobar, fiibar", + "traefik.routers.Router1.middlewares": "foobar, fiibar", + "traefik.routers.Router1.priority": "42", + "traefik.routers.Router1.rule": "foobar", + "traefik.routers.Router1.service": "foobar", + + "traefik.services.Service0.loadbalancer.healthcheck.headers.name0": "foobar", + "traefik.services.Service0.loadbalancer.healthcheck.headers.name1": "foobar", + "traefik.services.Service0.loadbalancer.healthcheck.hostname": "foobar", + "traefik.services.Service0.loadbalancer.healthcheck.interval": "foobar", + "traefik.services.Service0.loadbalancer.healthcheck.path": "foobar", + "traefik.services.Service0.loadbalancer.healthcheck.port": "42", + "traefik.services.Service0.loadbalancer.healthcheck.scheme": "foobar", + "traefik.services.Service0.loadbalancer.healthcheck.timeout": "foobar", + "traefik.services.Service0.loadbalancer.method": "foobar", + "traefik.services.Service0.loadbalancer.passhostheader": "true", + "traefik.services.Service0.loadbalancer.responseforwarding.flushinterval": "foobar", + "traefik.services.Service0.loadbalancer.server.url": "foobar", + "traefik.services.Service0.loadbalancer.server.weight": "42", + "traefik.services.Service0.loadbalancer.stickiness.cookiename": "foobar", + "traefik.services.Service1.loadbalancer.healthcheck.headers.name0": "foobar", + "traefik.services.Service1.loadbalancer.healthcheck.headers.name1": "foobar", + "traefik.services.Service1.loadbalancer.healthcheck.hostname": "foobar", + "traefik.services.Service1.loadbalancer.healthcheck.interval": "foobar", + "traefik.services.Service1.loadbalancer.healthcheck.path": "foobar", + "traefik.services.Service1.loadbalancer.healthcheck.port": "42", + "traefik.services.Service1.loadbalancer.healthcheck.scheme": "foobar", + "traefik.services.Service1.loadbalancer.healthcheck.timeout": "foobar", + "traefik.services.Service1.loadbalancer.method": "foobar", + "traefik.services.Service1.loadbalancer.passhostheader": "true", + "traefik.services.Service1.loadbalancer.responseforwarding.flushinterval": "foobar", + "traefik.services.Service1.loadbalancer.server.url": "foobar", + "traefik.services.Service1.loadbalancer.server.weight": "42", + "traefik.services.Service1.loadbalancer.stickiness": "false", + "traefik.services.Service1.loadbalancer.stickiness.cookiename": "fui", + } + + configuration, err := Decode(labels) + require.NoError(t, err) + + expected := &config.Configuration{ + Routers: map[string]*config.Router{ + "Router0": { + EntryPoints: []string{ + "foobar", + "fiibar", + }, + Middlewares: []string{ + "foobar", + "fiibar", + }, + Service: "foobar", + Rule: "foobar", + Priority: 42, + }, + "Router1": { + EntryPoints: []string{ + "foobar", + "fiibar", + }, + Middlewares: []string{ + "foobar", + "fiibar", + }, + Service: "foobar", + Rule: "foobar", + Priority: 42, + }, + }, + Middlewares: map[string]*config.Middleware{ + "Middleware0": { + AddPrefix: &config.AddPrefix{ + Prefix: "foobar", + }, + }, + "Middleware1": { + BasicAuth: &config.BasicAuth{ + Users: []string{ + "foobar", + "fiibar", + }, + UsersFile: "foobar", + Realm: "foobar", + RemoveHeader: true, + HeaderField: "foobar", + }, + }, + "Middleware10": { + MaxConn: &config.MaxConn{ + Amount: 42, + ExtractorFunc: "foobar", + }, + }, + "Middleware11": { + PassTLSClientCert: &config.PassTLSClientCert{ + PEM: true, + Infos: &config.TLSClientCertificateInfos{ + NotAfter: true, + NotBefore: true, + Subject: &config.TLSCLientCertificateSubjectInfos{ + Country: true, + Province: true, + Locality: true, + Organization: true, + CommonName: true, + SerialNumber: true, + }, + Sans: true, + }, + }, + }, + "Middleware12": { + RateLimit: &config.RateLimit{ + RateSet: map[string]*config.Rate{ + "Rate0": { + Period: parse.Duration(42 * time.Nanosecond), + Average: 42, + Burst: 42, + }, + "Rate1": { + Period: parse.Duration(42 * time.Nanosecond), + Average: 42, + Burst: 42, + }, + }, + ExtractorFunc: "foobar", + }, + }, + "Middleware13": { + Redirect: &config.Redirect{ + Regex: "foobar", + Replacement: "foobar", + Permanent: true, + }, + }, + "Middleware14": { + ReplacePath: &config.ReplacePath{ + Path: "foobar", + }, + }, + "Middleware15": { + ReplacePathRegex: &config.ReplacePathRegex{ + Regex: "foobar", + Replacement: "foobar", + }, + }, + "Middleware16": { + Retry: &config.Retry{ + Attempts: 42, + }, + }, + "Middleware17": { + StripPrefix: &config.StripPrefix{ + Prefixes: []string{ + "foobar", + "fiibar", + }, + }, + }, + "Middleware18": { + StripPrefixRegex: &config.StripPrefixRegex{ + Regex: []string{ + "foobar", + "fiibar", + }, + }, + }, + "Middleware19": { + Compress: &config.Compress{}, + }, + "Middleware2": { + Buffering: &config.Buffering{ + MaxRequestBodyBytes: 42, + MemRequestBodyBytes: 42, + MaxResponseBodyBytes: 42, + MemResponseBodyBytes: 42, + RetryExpression: "foobar", + }, + }, + "Middleware3": { + Chain: &config.Chain{ + Middlewares: []string{ + "foobar", + "fiibar", + }, + }, + }, + "Middleware4": { + CircuitBreaker: &config.CircuitBreaker{ + Expression: "foobar", + }, + }, + "Middleware5": { + DigestAuth: &config.DigestAuth{ + Users: []string{ + "foobar", + "fiibar", + }, + UsersFile: "foobar", + RemoveHeader: true, + Realm: "foobar", + HeaderField: "foobar", + }, + }, + "Middleware6": { + Errors: &config.ErrorPage{ + Status: []string{ + "foobar", + "fiibar", + }, + Service: "foobar", + Query: "foobar", + }, + }, + "Middleware7": { + ForwardAuth: &config.ForwardAuth{ + Address: "foobar", + TLS: &config.ClientTLS{ + CA: "foobar", + CAOptional: true, + Cert: "foobar", + Key: "foobar", + InsecureSkipVerify: true, + }, + TrustForwardHeader: true, + AuthResponseHeaders: []string{ + "foobar", + "fiibar", + }, + }, + }, + "Middleware8": { + Headers: &config.Headers{ + CustomRequestHeaders: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + CustomResponseHeaders: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + AllowedHosts: []string{ + "foobar", + "fiibar", + }, + HostsProxyHeaders: []string{ + "foobar", + "fiibar", + }, + SSLRedirect: true, + SSLTemporaryRedirect: true, + SSLHost: "foobar", + SSLProxyHeaders: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + SSLForceHost: true, + STSSeconds: 42, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foobar", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foobar", + ContentSecurityPolicy: "foobar", + PublicKey: "foobar", + ReferrerPolicy: "foobar", + IsDevelopment: true, + }, + }, + "Middleware9": { + IPWhiteList: &config.IPWhiteList{ + SourceRange: []string{ + "foobar", + "fiibar", + }, + IPStrategy: &config.IPStrategy{ + Depth: 42, + ExcludedIPs: []string{ + "foobar", + "fiibar", + }, + }, + }, + }, + }, + Services: map[string]*config.Service{ + "Service0": { + LoadBalancer: &config.LoadBalancerService{ + Stickiness: &config.Stickiness{ + CookieName: "foobar", + }, + Servers: []config.Server{ + { + URL: "foobar", + Weight: 42, + }, + }, + Method: "foobar", + HealthCheck: &config.HealthCheck{ + Scheme: "foobar", + Path: "foobar", + Port: 42, + Interval: "foobar", + Timeout: "foobar", + Hostname: "foobar", + Headers: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + }, + PassHostHeader: true, + ResponseForwarding: &config.ResponseForwarding{ + FlushInterval: "foobar", + }, + }, + }, + "Service1": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "foobar", + Weight: 42, + }, + }, + Method: "foobar", + HealthCheck: &config.HealthCheck{ + Scheme: "foobar", + Path: "foobar", + Port: 42, + Interval: "foobar", + Timeout: "foobar", + Hostname: "foobar", + Headers: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + }, + PassHostHeader: true, + ResponseForwarding: &config.ResponseForwarding{ + FlushInterval: "foobar", + }, + }, + }, + }, + } + + assert.Equal(t, expected, configuration) +} + +func TestEncode(t *testing.T) { + configuration := &config.Configuration{ + Routers: map[string]*config.Router{ + "Router0": { + EntryPoints: []string{ + "foobar", + "fiibar", + }, + Middlewares: []string{ + "foobar", + "fiibar", + }, + Service: "foobar", + Rule: "foobar", + Priority: 42, + }, + "Router1": { + EntryPoints: []string{ + "foobar", + "fiibar", + }, + Middlewares: []string{ + "foobar", + "fiibar", + }, + Service: "foobar", + Rule: "foobar", + Priority: 42, + }, + }, + Middlewares: map[string]*config.Middleware{ + "Middleware0": { + AddPrefix: &config.AddPrefix{ + Prefix: "foobar", + }, + }, + "Middleware1": { + BasicAuth: &config.BasicAuth{ + Users: []string{ + "foobar", + "fiibar", + }, + UsersFile: "foobar", + Realm: "foobar", + RemoveHeader: true, + HeaderField: "foobar", + }, + }, + "Middleware10": { + MaxConn: &config.MaxConn{ + Amount: 42, + ExtractorFunc: "foobar", + }, + }, + "Middleware11": { + PassTLSClientCert: &config.PassTLSClientCert{ + PEM: true, + Infos: &config.TLSClientCertificateInfos{ + NotAfter: true, + NotBefore: true, + Subject: &config.TLSCLientCertificateSubjectInfos{ + Country: true, + Province: true, + Locality: true, + Organization: true, + CommonName: true, + SerialNumber: true, + }, + Sans: true, + }, + }, + }, + "Middleware12": { + RateLimit: &config.RateLimit{ + RateSet: map[string]*config.Rate{ + "Rate0": { + Period: parse.Duration(42 * time.Nanosecond), + Average: 42, + Burst: 42, + }, + "Rate1": { + Period: parse.Duration(42 * time.Nanosecond), + Average: 42, + Burst: 42, + }, + }, + ExtractorFunc: "foobar", + }, + }, + "Middleware13": { + Redirect: &config.Redirect{ + Regex: "foobar", + Replacement: "foobar", + Permanent: true, + }, + }, + "Middleware14": { + ReplacePath: &config.ReplacePath{ + Path: "foobar", + }, + }, + "Middleware15": { + ReplacePathRegex: &config.ReplacePathRegex{ + Regex: "foobar", + Replacement: "foobar", + }, + }, + "Middleware16": { + Retry: &config.Retry{ + Attempts: 42, + }, + }, + "Middleware17": { + StripPrefix: &config.StripPrefix{ + Prefixes: []string{ + "foobar", + "fiibar", + }, + }, + }, + "Middleware18": { + StripPrefixRegex: &config.StripPrefixRegex{ + Regex: []string{ + "foobar", + "fiibar", + }, + }, + }, + "Middleware19": { + Compress: &config.Compress{}, + }, + "Middleware2": { + Buffering: &config.Buffering{ + MaxRequestBodyBytes: 42, + MemRequestBodyBytes: 42, + MaxResponseBodyBytes: 42, + MemResponseBodyBytes: 42, + RetryExpression: "foobar", + }, + }, + "Middleware3": { + Chain: &config.Chain{ + Middlewares: []string{ + "foobar", + "fiibar", + }, + }, + }, + "Middleware4": { + CircuitBreaker: &config.CircuitBreaker{ + Expression: "foobar", + }, + }, + "Middleware5": { + DigestAuth: &config.DigestAuth{ + Users: []string{ + "foobar", + "fiibar", + }, + UsersFile: "foobar", + RemoveHeader: true, + Realm: "foobar", + HeaderField: "foobar", + }, + }, + "Middleware6": { + Errors: &config.ErrorPage{ + Status: []string{ + "foobar", + "fiibar", + }, + Service: "foobar", + Query: "foobar", + }, + }, + "Middleware7": { + ForwardAuth: &config.ForwardAuth{ + Address: "foobar", + TLS: &config.ClientTLS{ + CA: "foobar", + CAOptional: true, + Cert: "foobar", + Key: "foobar", + InsecureSkipVerify: true, + }, + TrustForwardHeader: true, + AuthResponseHeaders: []string{ + "foobar", + "fiibar", + }, + }, + }, + "Middleware8": { + Headers: &config.Headers{ + CustomRequestHeaders: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + CustomResponseHeaders: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + AllowedHosts: []string{ + "foobar", + "fiibar", + }, + HostsProxyHeaders: []string{ + "foobar", + "fiibar", + }, + SSLRedirect: true, + SSLTemporaryRedirect: true, + SSLHost: "foobar", + SSLProxyHeaders: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + SSLForceHost: true, + STSSeconds: 42, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foobar", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + CustomBrowserXSSValue: "foobar", + ContentSecurityPolicy: "foobar", + PublicKey: "foobar", + ReferrerPolicy: "foobar", + IsDevelopment: true, + }, + }, + "Middleware9": { + IPWhiteList: &config.IPWhiteList{ + SourceRange: []string{ + "foobar", + "fiibar", + }, + IPStrategy: &config.IPStrategy{ + Depth: 42, + ExcludedIPs: []string{ + "foobar", + "fiibar", + }, + }, + }, + }, + }, + Services: map[string]*config.Service{ + "Service0": { + LoadBalancer: &config.LoadBalancerService{ + Stickiness: &config.Stickiness{ + CookieName: "foobar", + }, + Servers: []config.Server{ + { + URL: "foobar", + Weight: 42, + }, + }, + Method: "foobar", + HealthCheck: &config.HealthCheck{ + Scheme: "foobar", + Path: "foobar", + Port: 42, + Interval: "foobar", + Timeout: "foobar", + Hostname: "foobar", + Headers: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + }, + PassHostHeader: true, + ResponseForwarding: &config.ResponseForwarding{ + FlushInterval: "foobar", + }, + }, + }, + "Service1": { + LoadBalancer: &config.LoadBalancerService{ + Servers: []config.Server{ + { + URL: "foobar", + Weight: 42, + }, + }, + Method: "foobar", + HealthCheck: &config.HealthCheck{ + Scheme: "foobar", + Path: "foobar", + Port: 42, + Interval: "foobar", + Timeout: "foobar", + Hostname: "foobar", + Headers: map[string]string{ + "name0": "foobar", + "name1": "foobar", + }, + }, + PassHostHeader: true, + ResponseForwarding: &config.ResponseForwarding{ + FlushInterval: "foobar", + }, + }, + }, + }, + } + + labels, err := Encode(configuration) + require.NoError(t, err) + + expected := map[string]string{ + "traefik.Middlewares.Middleware0.AddPrefix.Prefix": "foobar", + "traefik.Middlewares.Middleware1.BasicAuth.HeaderField": "foobar", + "traefik.Middlewares.Middleware1.BasicAuth.Realm": "foobar", + "traefik.Middlewares.Middleware1.BasicAuth.RemoveHeader": "true", + "traefik.Middlewares.Middleware1.BasicAuth.Users": "foobar, fiibar", + "traefik.Middlewares.Middleware1.BasicAuth.UsersFile": "foobar", + "traefik.Middlewares.Middleware2.Buffering.MaxRequestBodyBytes": "42", + "traefik.Middlewares.Middleware2.Buffering.MaxResponseBodyBytes": "42", + "traefik.Middlewares.Middleware2.Buffering.MemRequestBodyBytes": "42", + "traefik.Middlewares.Middleware2.Buffering.MemResponseBodyBytes": "42", + "traefik.Middlewares.Middleware2.Buffering.RetryExpression": "foobar", + "traefik.Middlewares.Middleware3.Chain.Middlewares": "foobar, fiibar", + "traefik.Middlewares.Middleware4.CircuitBreaker.Expression": "foobar", + "traefik.Middlewares.Middleware5.DigestAuth.HeaderField": "foobar", + "traefik.Middlewares.Middleware5.DigestAuth.Realm": "foobar", + "traefik.Middlewares.Middleware5.DigestAuth.RemoveHeader": "true", + "traefik.Middlewares.Middleware5.DigestAuth.Users": "foobar, fiibar", + "traefik.Middlewares.Middleware5.DigestAuth.UsersFile": "foobar", + "traefik.Middlewares.Middleware6.Errors.Query": "foobar", + "traefik.Middlewares.Middleware6.Errors.Service": "foobar", + "traefik.Middlewares.Middleware6.Errors.Status": "foobar, fiibar", + "traefik.Middlewares.Middleware7.ForwardAuth.Address": "foobar", + "traefik.Middlewares.Middleware7.ForwardAuth.AuthResponseHeaders": "foobar, fiibar", + "traefik.Middlewares.Middleware7.ForwardAuth.TLS.CA": "foobar", + "traefik.Middlewares.Middleware7.ForwardAuth.TLS.CAOptional": "true", + "traefik.Middlewares.Middleware7.ForwardAuth.TLS.Cert": "foobar", + "traefik.Middlewares.Middleware7.ForwardAuth.TLS.InsecureSkipVerify": "true", + "traefik.Middlewares.Middleware7.ForwardAuth.TLS.Key": "foobar", + "traefik.Middlewares.Middleware7.ForwardAuth.TrustForwardHeader": "true", + "traefik.Middlewares.Middleware8.Headers.AllowedHosts": "foobar, fiibar", + "traefik.Middlewares.Middleware8.Headers.BrowserXSSFilter": "true", + "traefik.Middlewares.Middleware8.Headers.ContentSecurityPolicy": "foobar", + "traefik.Middlewares.Middleware8.Headers.ContentTypeNosniff": "true", + "traefik.Middlewares.Middleware8.Headers.CustomBrowserXSSValue": "foobar", + "traefik.Middlewares.Middleware8.Headers.CustomFrameOptionsValue": "foobar", + "traefik.Middlewares.Middleware8.Headers.CustomRequestHeaders.name0": "foobar", + "traefik.Middlewares.Middleware8.Headers.CustomRequestHeaders.name1": "foobar", + "traefik.Middlewares.Middleware8.Headers.CustomResponseHeaders.name0": "foobar", + "traefik.Middlewares.Middleware8.Headers.CustomResponseHeaders.name1": "foobar", + "traefik.Middlewares.Middleware8.Headers.ForceSTSHeader": "true", + "traefik.Middlewares.Middleware8.Headers.FrameDeny": "true", + "traefik.Middlewares.Middleware8.Headers.HostsProxyHeaders": "foobar, fiibar", + "traefik.Middlewares.Middleware8.Headers.IsDevelopment": "true", + "traefik.Middlewares.Middleware8.Headers.PublicKey": "foobar", + "traefik.Middlewares.Middleware8.Headers.ReferrerPolicy": "foobar", + "traefik.Middlewares.Middleware8.Headers.SSLForceHost": "true", + "traefik.Middlewares.Middleware8.Headers.SSLHost": "foobar", + "traefik.Middlewares.Middleware8.Headers.SSLProxyHeaders.name0": "foobar", + "traefik.Middlewares.Middleware8.Headers.SSLProxyHeaders.name1": "foobar", + "traefik.Middlewares.Middleware8.Headers.SSLRedirect": "true", + "traefik.Middlewares.Middleware8.Headers.SSLTemporaryRedirect": "true", + "traefik.Middlewares.Middleware8.Headers.STSIncludeSubdomains": "true", + "traefik.Middlewares.Middleware8.Headers.STSPreload": "true", + "traefik.Middlewares.Middleware8.Headers.STSSeconds": "42", + "traefik.Middlewares.Middleware9.IPWhiteList.IPStrategy.Depth": "42", + "traefik.Middlewares.Middleware9.IPWhiteList.IPStrategy.ExcludedIPs": "foobar, fiibar", + "traefik.Middlewares.Middleware9.IPWhiteList.SourceRange": "foobar, fiibar", + "traefik.Middlewares.Middleware10.MaxConn.Amount": "42", + "traefik.Middlewares.Middleware10.MaxConn.ExtractorFunc": "foobar", + "traefik.Middlewares.Middleware11.PassTLSClientCert.Infos.NotAfter": "true", + "traefik.Middlewares.Middleware11.PassTLSClientCert.Infos.NotBefore": "true", + "traefik.Middlewares.Middleware11.PassTLSClientCert.Infos.Sans": "true", + "traefik.Middlewares.Middleware11.PassTLSClientCert.Infos.Subject.CommonName": "true", + "traefik.Middlewares.Middleware11.PassTLSClientCert.Infos.Subject.Country": "true", + "traefik.Middlewares.Middleware11.PassTLSClientCert.Infos.Subject.Locality": "true", + "traefik.Middlewares.Middleware11.PassTLSClientCert.Infos.Subject.Organization": "true", + "traefik.Middlewares.Middleware11.PassTLSClientCert.Infos.Subject.Province": "true", + "traefik.Middlewares.Middleware11.PassTLSClientCert.Infos.Subject.SerialNumber": "true", + "traefik.Middlewares.Middleware11.PassTLSClientCert.PEM": "true", + "traefik.Middlewares.Middleware12.RateLimit.ExtractorFunc": "foobar", + "traefik.Middlewares.Middleware12.RateLimit.RateSet.Rate0.Average": "42", + "traefik.Middlewares.Middleware12.RateLimit.RateSet.Rate0.Burst": "42", + "traefik.Middlewares.Middleware12.RateLimit.RateSet.Rate0.Period": "42", + "traefik.Middlewares.Middleware12.RateLimit.RateSet.Rate1.Average": "42", + "traefik.Middlewares.Middleware12.RateLimit.RateSet.Rate1.Burst": "42", + "traefik.Middlewares.Middleware12.RateLimit.RateSet.Rate1.Period": "42", + "traefik.Middlewares.Middleware13.Redirect.Permanent": "true", + "traefik.Middlewares.Middleware13.Redirect.Regex": "foobar", + "traefik.Middlewares.Middleware13.Redirect.Replacement": "foobar", + "traefik.Middlewares.Middleware14.ReplacePath.Path": "foobar", + "traefik.Middlewares.Middleware15.ReplacePathRegex.Regex": "foobar", + "traefik.Middlewares.Middleware15.ReplacePathRegex.Replacement": "foobar", + "traefik.Middlewares.Middleware16.Retry.Attempts": "42", + "traefik.Middlewares.Middleware17.StripPrefix.Prefixes": "foobar, fiibar", + "traefik.Middlewares.Middleware18.StripPrefixRegex.Regex": "foobar, fiibar", + "traefik.Middlewares.Middleware19.Compress": "true", + + "traefik.Routers.Router0.EntryPoints": "foobar, fiibar", + "traefik.Routers.Router0.Middlewares": "foobar, fiibar", + "traefik.Routers.Router0.Priority": "42", + "traefik.Routers.Router0.Rule": "foobar", + "traefik.Routers.Router0.Service": "foobar", + "traefik.Routers.Router1.EntryPoints": "foobar, fiibar", + "traefik.Routers.Router1.Middlewares": "foobar, fiibar", + "traefik.Routers.Router1.Priority": "42", + "traefik.Routers.Router1.Rule": "foobar", + "traefik.Routers.Router1.Service": "foobar", + + "traefik.Services.Service0.LoadBalancer.HealthCheck.Headers.name0": "foobar", + "traefik.Services.Service0.LoadBalancer.HealthCheck.Headers.name1": "foobar", + "traefik.Services.Service0.LoadBalancer.HealthCheck.Hostname": "foobar", + "traefik.Services.Service0.LoadBalancer.HealthCheck.Interval": "foobar", + "traefik.Services.Service0.LoadBalancer.HealthCheck.Path": "foobar", + "traefik.Services.Service0.LoadBalancer.HealthCheck.Port": "42", + "traefik.Services.Service0.LoadBalancer.HealthCheck.Scheme": "foobar", + "traefik.Services.Service0.LoadBalancer.HealthCheck.Timeout": "foobar", + "traefik.Services.Service0.LoadBalancer.Method": "foobar", + "traefik.Services.Service0.LoadBalancer.PassHostHeader": "true", + "traefik.Services.Service0.LoadBalancer.ResponseForwarding.FlushInterval": "foobar", + "traefik.Services.Service0.LoadBalancer.server.URL": "foobar", + "traefik.Services.Service0.LoadBalancer.server.Weight": "42", + "traefik.Services.Service0.LoadBalancer.Stickiness.CookieName": "foobar", + "traefik.Services.Service1.LoadBalancer.HealthCheck.Headers.name0": "foobar", + "traefik.Services.Service1.LoadBalancer.HealthCheck.Headers.name1": "foobar", + "traefik.Services.Service1.LoadBalancer.HealthCheck.Hostname": "foobar", + "traefik.Services.Service1.LoadBalancer.HealthCheck.Interval": "foobar", + "traefik.Services.Service1.LoadBalancer.HealthCheck.Path": "foobar", + "traefik.Services.Service1.LoadBalancer.HealthCheck.Port": "42", + "traefik.Services.Service1.LoadBalancer.HealthCheck.Scheme": "foobar", + "traefik.Services.Service1.LoadBalancer.HealthCheck.Timeout": "foobar", + "traefik.Services.Service1.LoadBalancer.Method": "foobar", + "traefik.Services.Service1.LoadBalancer.PassHostHeader": "true", + "traefik.Services.Service1.LoadBalancer.ResponseForwarding.FlushInterval": "foobar", + "traefik.Services.Service1.LoadBalancer.server.URL": "foobar", + "traefik.Services.Service1.LoadBalancer.server.Weight": "42", + } + + for key, val := range expected { + if _, ok := labels[key]; !ok { + fmt.Println("missing in labels:", key, val) + } + } + + for key, val := range labels { + if _, ok := expected[key]; !ok { + fmt.Println("missing in expected:", key, val) + } + } + assert.Equal(t, expected, labels) +}