Filter env vars configuration

This commit is contained in:
Ludovic Fernandez 2019-06-21 10:08:04 +02:00 committed by Traefiker Bot
parent adc9a65ae3
commit a918dcd5a4
19 changed files with 284 additions and 79 deletions

View file

@ -70,6 +70,12 @@ docker run traefik[:version] --help
# ex: docker run traefik:2.0 --help # ex: docker run traefik:2.0 --help
``` ```
All available arguments can also be found [here](../reference/static-configuration/cli.md).
### Environment Variables
All available environment variables can be found [here](../reference/static-configuration/env.md)
## Available Configuration Options ## Available Configuration Options
All the configuration options are documented in their related section. All the configuration options are documented in their related section.

View file

@ -3,7 +3,6 @@ package cli
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"github.com/containous/traefik/pkg/config/env" "github.com/containous/traefik/pkg/config/env"
"github.com/containous/traefik/pkg/log" "github.com/containous/traefik/pkg/log"
@ -14,23 +13,12 @@ type EnvLoader struct{}
// Load loads the command's configuration from the environment variables. // Load loads the command's configuration from the environment variables.
func (e *EnvLoader) Load(_ []string, cmd *Command) (bool, error) { func (e *EnvLoader) Load(_ []string, cmd *Command) (bool, error) {
return e.load(os.Environ(), cmd) vars := env.FindPrefixedEnvVars(os.Environ(), env.DefaultNamePrefix, cmd.Configuration)
} if len(vars) == 0 {
func (*EnvLoader) load(environ []string, cmd *Command) (bool, error) {
var found bool
for _, value := range environ {
if strings.HasPrefix(value, "TRAEFIK_") {
found = true
break
}
}
if !found {
return false, nil return false, nil
} }
if err := env.Decode(environ, cmd.Configuration); err != nil { if err := env.Decode(vars, env.DefaultNamePrefix, cmd.Configuration); err != nil {
return false, fmt.Errorf("failed to decode configuration from environment variables: %v", err) return false, fmt.Errorf("failed to decode configuration from environment variables: %v", err)
} }

32
pkg/config/env/env.go vendored
View file

@ -2,28 +2,38 @@
package env package env
import ( import (
"fmt"
"regexp"
"strings" "strings"
"github.com/containous/traefik/pkg/config/parser" "github.com/containous/traefik/pkg/config/parser"
) )
// DefaultNamePrefix is the default prefix for environment variable names.
const DefaultNamePrefix = "TRAEFIK_"
// Decode decodes the given environment variables into the given element. // Decode decodes the given environment variables into the given element.
// The operation goes through four stages roughly summarized as: // The operation goes through four stages roughly summarized as:
// env vars -> map // env vars -> map
// map -> tree of untyped nodes // map -> tree of untyped nodes
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element) // untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
// "typed" nodes -> typed element // "typed" nodes -> typed element
func Decode(environ []string, element interface{}) error { func Decode(environ []string, prefix string, element interface{}) error {
if err := checkPrefix(prefix); err != nil {
return err
}
vars := make(map[string]string) vars := make(map[string]string)
for _, evr := range environ { for _, evr := range environ {
n := strings.SplitN(evr, "=", 2) n := strings.SplitN(evr, "=", 2)
if strings.HasPrefix(strings.ToUpper(n[0]), "TRAEFIK_") { if strings.HasPrefix(strings.ToUpper(n[0]), prefix) {
key := strings.ReplaceAll(strings.ToLower(n[0]), "_", ".") key := strings.ReplaceAll(strings.ToLower(n[0]), "_", ".")
vars[key] = n[1] vars[key] = n[1]
} }
} }
return parser.Decode(vars, element) rootName := strings.ToLower(prefix[:len(prefix)-1])
return parser.Decode(vars, element, rootName)
} }
// Encode encodes the configuration in element into the environment variables represented in the returned Flats. // Encode encodes the configuration in element into the environment variables represented in the returned Flats.
@ -36,7 +46,7 @@ func Encode(element interface{}) ([]parser.Flat, error) {
return nil, nil return nil, nil
} }
node, err := parser.EncodeToNode(element, false) node, err := parser.EncodeToNode(element, parser.DefaultRootName, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -48,3 +58,17 @@ func Encode(element interface{}) ([]parser.Flat, error) {
return parser.EncodeToFlat(element, node, parser.FlatOpts{Case: "upper", Separator: "_"}) return parser.EncodeToFlat(element, node, parser.FlatOpts{Case: "upper", Separator: "_"})
} }
func checkPrefix(prefix string) error {
prefixPattern := `[a-zA-Z0-9]+_`
matched, err := regexp.MatchString(prefixPattern, prefix)
if err != nil {
return err
}
if !matched {
return fmt.Errorf("invalid prefix %q, the prefix pattern must match the following pattern: %s", prefix, prefixPattern)
}
return nil
}

View file

@ -173,7 +173,7 @@ func TestDecode(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
err := Decode(test.environ, test.element) err := Decode(test.environ, DefaultNamePrefix, test.element)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, test.expected, test.element) assert.Equal(t, test.expected, test.element)
@ -460,39 +460,3 @@ func TestEncode(t *testing.T) {
assert.Equal(t, expected, flats) assert.Equal(t, expected, flats)
} }
type Ya struct {
Foo *Yaa
Field1 string
Field2 bool
Field3 int
Field4 map[string]string
Field5 map[string]int
Field6 map[string]struct{ Field string }
Field7 map[string]struct{ Field map[string]string }
Field8 map[string]*struct{ Field string }
Field9 map[string]*struct{ Field map[string]string }
Field10 struct{ Field string }
Field11 *struct{ Field string }
Field12 *string
Field13 *bool
Field14 *int
Field15 []int
}
type Yaa struct {
FieldIn1 string
FieldIn2 bool
FieldIn3 int
FieldIn4 map[string]string
FieldIn5 map[string]int
FieldIn6 map[string]struct{ Field string }
FieldIn7 map[string]struct{ Field map[string]string }
FieldIn8 map[string]*struct{ Field string }
FieldIn9 map[string]*struct{ Field map[string]string }
FieldIn10 struct{ Field string }
FieldIn11 *struct{ Field string }
FieldIn12 *string
FieldIn13 *bool
FieldIn14 *int
}

64
pkg/config/env/filter.go vendored Normal file
View file

@ -0,0 +1,64 @@
package env
import (
"reflect"
"strings"
"github.com/containous/traefik/pkg/config/parser"
)
// FindPrefixedEnvVars finds prefixed environment variables.
func FindPrefixedEnvVars(environ []string, prefix string, element interface{}) []string {
prefixes := getRootPrefixes(element, prefix)
var values []string
for _, px := range prefixes {
for _, value := range environ {
if strings.HasPrefix(value, px) {
values = append(values, value)
}
}
}
return values
}
func getRootPrefixes(element interface{}, prefix string) []string {
if element == nil {
return nil
}
rootType := reflect.TypeOf(element)
return getPrefixes(prefix, rootType)
}
func getPrefixes(prefix string, rootType reflect.Type) []string {
var names []string
if rootType.Kind() == reflect.Ptr {
rootType = rootType.Elem()
}
if rootType.Kind() != reflect.Struct {
return nil
}
for i := 0; i < rootType.NumField(); i++ {
field := rootType.Field(i)
if !parser.IsExported(field) {
continue
}
if field.Anonymous &&
(field.Type.Kind() == reflect.Ptr && field.Type.Elem().Kind() == reflect.Struct || field.Type.Kind() == reflect.Struct) {
names = append(names, getPrefixes(prefix, field.Type)...)
continue
}
names = append(names, prefix+strings.ToUpper(field.Name))
}
return names
}

87
pkg/config/env/filter_test.go vendored Normal file
View file

@ -0,0 +1,87 @@
package env
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestFindPrefixedEnvVars(t *testing.T) {
testCases := []struct {
desc string
environ []string
element interface{}
expected []string
}{
{
desc: "exact name",
environ: []string{"TRAEFIK_FOO"},
element: &Yo{},
expected: []string{"TRAEFIK_FOO"},
},
{
desc: "prefixed name",
environ: []string{"TRAEFIK_FII01"},
element: &Yo{},
expected: []string{"TRAEFIK_FII01"},
},
{
desc: "excluded env vars",
environ: []string{"TRAEFIK_NOPE", "TRAEFIK_NO"},
element: &Yo{},
expected: nil,
},
{
desc: "filter",
environ: []string{"TRAEFIK_NOPE", "TRAEFIK_NO", "TRAEFIK_FOO", "TRAEFIK_FII01"},
element: &Yo{},
expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII01"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
vars := FindPrefixedEnvVars(test.environ, DefaultNamePrefix, test.element)
assert.Equal(t, test.expected, vars)
})
}
}
func Test_getRootFieldNames(t *testing.T) {
testCases := []struct {
desc string
element interface{}
expected []string
}{
{
desc: "simple fields",
element: &Yo{},
expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII", "TRAEFIK_FUU", "TRAEFIK_YI", "TRAEFIK_YU"},
},
{
desc: "embedded struct",
element: &Yu{},
expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII", "TRAEFIK_FUU"},
},
{
desc: "embedded struct pointer",
element: &Ye{},
expected: []string{"TRAEFIK_FOO", "TRAEFIK_FII", "TRAEFIK_FUU"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
names := getRootPrefixes(test.element, DefaultNamePrefix)
assert.Equal(t, test.expected, names)
})
}
}

69
pkg/config/env/fixtures_test.go vendored Normal file
View file

@ -0,0 +1,69 @@
package env
type Ya struct {
Foo *Yaa
Field1 string
Field2 bool
Field3 int
Field4 map[string]string
Field5 map[string]int
Field6 map[string]struct{ Field string }
Field7 map[string]struct{ Field map[string]string }
Field8 map[string]*struct{ Field string }
Field9 map[string]*struct{ Field map[string]string }
Field10 struct{ Field string }
Field11 *struct{ Field string }
Field12 *string
Field13 *bool
Field14 *int
Field15 []int
}
type Yaa struct {
FieldIn1 string
FieldIn2 bool
FieldIn3 int
FieldIn4 map[string]string
FieldIn5 map[string]int
FieldIn6 map[string]struct{ Field string }
FieldIn7 map[string]struct{ Field map[string]string }
FieldIn8 map[string]*struct{ Field string }
FieldIn9 map[string]*struct{ Field map[string]string }
FieldIn10 struct{ Field string }
FieldIn11 *struct{ Field string }
FieldIn12 *string
FieldIn13 *bool
FieldIn14 *int
}
type Yo struct {
Foo string `description:"Foo description"`
Fii string `description:"Fii description"`
Fuu string `description:"Fuu description"`
Yi *Yi `label:"allowEmpty"`
Yu *Yi
}
func (y *Yo) SetDefaults() {
y.Foo = "foo"
y.Fii = "fii"
}
type Yi struct {
Foo string
Fii string
Fuu string
}
func (y *Yi) SetDefaults() {
y.Foo = "foo"
y.Fii = "fii"
}
type Yu struct {
Yi
}
type Ye struct {
*Yi
}

View file

@ -36,13 +36,13 @@ func decodeFileToNode(filePath string, filters ...string) (*parser.Node, error)
return nil, err return nil, err
} }
return decodeRawToNode(data, filters...) return decodeRawToNode(data, parser.DefaultRootName, filters...)
default: default:
return nil, fmt.Errorf("unsupported file extension: %s", filePath) return nil, fmt.Errorf("unsupported file extension: %s", filePath)
} }
return decodeRawToNode(data, filters...) return decodeRawToNode(data, parser.DefaultRootName, filters...)
} }
func getRootFieldNames(element interface{}) []string { func getRootFieldNames(element interface{}) []string {

View file

@ -9,9 +9,9 @@ import (
"github.com/containous/traefik/pkg/config/parser" "github.com/containous/traefik/pkg/config/parser"
) )
func decodeRawToNode(data map[string]interface{}, filters ...string) (*parser.Node, error) { func decodeRawToNode(data map[string]interface{}, rootName string, filters ...string) (*parser.Node, error) {
root := &parser.Node{ root := &parser.Node{
Name: "traefik", Name: rootName,
} }
vData := reflect.ValueOf(data) vData := reflect.ValueOf(data)

View file

@ -531,7 +531,7 @@ func Test_decodeRawToNode(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
node, err := decodeRawToNode(test.data) node, err := decodeRawToNode(test.data, parser.DefaultRootName)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, test.expected, node) assert.Equal(t, test.expected, node)

View file

@ -17,7 +17,7 @@ func Decode(args []string, element interface{}) error {
return err return err
} }
return parser.Decode(ref, element) return parser.Decode(ref, element, parser.DefaultRootName)
} }
// Encode encodes the configuration in element into the flags represented in the returned Flats. // Encode encodes the configuration in element into the flags represented in the returned Flats.
@ -30,7 +30,7 @@ func Encode(element interface{}) ([]parser.Flat, error) {
return nil, nil return nil, nil
} }
node, err := parser.EncodeToNode(element, false) node, err := parser.EncodeToNode(element, parser.DefaultRootName, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
"github.com/containous/traefik/pkg/config/parser"
) )
// Parse parses the command-line flag arguments into a map, // Parse parses the command-line flag arguments into a map,
@ -96,7 +98,7 @@ func (f *flagSet) parseOne() (bool, error) {
} }
func (f *flagSet) setValue(name string, value string) { func (f *flagSet) setValue(name string, value string) {
n := strings.ToLower("traefik." + name) n := strings.ToLower(parser.DefaultRootName + "." + name)
v, ok := f.values[n] v, ok := f.values[n]
if ok && f.flagTypes[name] == reflect.Slice { if ok && f.flagTypes[name] == reflect.Slice {

View file

@ -13,7 +13,7 @@ func DecodeConfiguration(labels map[string]string) (*config.Configuration, error
TCP: &config.TCPConfiguration{}, TCP: &config.TCPConfiguration{},
} }
err := parser.Decode(labels, conf, "traefik.http", "traefik.tcp") err := parser.Decode(labels, conf, parser.DefaultRootName, "traefik.http", "traefik.tcp")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -23,11 +23,11 @@ func DecodeConfiguration(labels map[string]string) (*config.Configuration, error
// EncodeConfiguration converts a configuration to labels. // EncodeConfiguration converts a configuration to labels.
func EncodeConfiguration(conf *config.Configuration) (map[string]string, error) { func EncodeConfiguration(conf *config.Configuration) (map[string]string, error) {
return parser.Encode(conf) return parser.Encode(conf, parser.DefaultRootName)
} }
// Decode converts the labels to an element. // Decode converts the labels to an element.
// labels -> [ node -> node + metadata (type) ] -> element (node) // labels -> [ node -> node + metadata (type) ] -> element (node)
func Decode(labels map[string]string, element interface{}, filters ...string) error { func Decode(labels map[string]string, element interface{}, filters ...string) error {
return parser.Decode(labels, element, filters...) return parser.Decode(labels, element, parser.DefaultRootName, filters...)
} }

View file

@ -9,9 +9,9 @@ import (
// EncodeToNode converts an element to a node. // EncodeToNode converts an element to a node.
// element -> nodes // element -> nodes
func EncodeToNode(element interface{}, omitEmpty bool) (*Node, error) { func EncodeToNode(element interface{}, rootName string, omitEmpty bool) (*Node, error) {
rValue := reflect.ValueOf(element) rValue := reflect.ValueOf(element)
node := &Node{Name: "traefik"} node := &Node{Name: rootName}
encoder := encoderToNode{omitEmpty: omitEmpty} encoder := encoderToNode{omitEmpty: omitEmpty}

View file

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

View file

@ -6,18 +6,16 @@ import (
"strings" "strings"
) )
const labelRoot = "traefik"
// DecodeToNode converts the labels to a tree of nodes. // DecodeToNode converts the labels to a tree of nodes.
// If any filters are present, labels which do not match the filters are skipped. // If any filters are present, labels which do not match the filters are skipped.
func DecodeToNode(labels map[string]string, filters ...string) (*Node, error) { func DecodeToNode(labels map[string]string, rootName string, filters ...string) (*Node, error) {
sortedKeys := sortKeys(labels, filters) sortedKeys := sortKeys(labels, filters)
var node *Node var node *Node
for i, key := range sortedKeys { for i, key := range sortedKeys {
split := strings.Split(key, ".") split := strings.Split(key, ".")
if split[0] != labelRoot { if split[0] != rootName {
return nil, fmt.Errorf("invalid label root %s", split[0]) return nil, fmt.Errorf("invalid label root %s", split[0])
} }

View file

@ -218,7 +218,7 @@ func TestDecodeToNode(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
out, err := DecodeToNode(test.in, test.filters...) out, err := DecodeToNode(test.in, DefaultRootName, test.filters...)
if test.expected.error { if test.expected.error {
require.Error(t, err) require.Error(t, err)

View file

@ -2,6 +2,9 @@ package parser
import "reflect" import "reflect"
// DefaultRootName is the default name of the root node and the prefix of element name from the resources.
const DefaultRootName = "traefik"
// MapNamePlaceholder is the placeholder for the map name. // MapNamePlaceholder is the placeholder for the map name.
const MapNamePlaceholder = "<name>" const MapNamePlaceholder = "<name>"

View file

@ -7,8 +7,8 @@ package parser
// labels -> tree of untyped nodes // labels -> tree of untyped nodes
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element) // untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
// "typed" nodes -> typed element // "typed" nodes -> typed element
func Decode(labels map[string]string, element interface{}, filters ...string) error { func Decode(labels map[string]string, element interface{}, rootName string, filters ...string) error {
node, err := DecodeToNode(labels, filters...) node, err := DecodeToNode(labels, rootName, filters...)
if err != nil { if err != nil {
return err return err
} }
@ -28,8 +28,8 @@ func Decode(labels map[string]string, element interface{}, filters ...string) er
// Encode converts an element to labels. // Encode converts an element to labels.
// element -> node (value) -> label (node) // element -> node (value) -> label (node)
func Encode(element interface{}) (map[string]string, error) { func Encode(element interface{}, rootName string) (map[string]string, error) {
node, err := EncodeToNode(element, true) node, err := EncodeToNode(element, rootName, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }