diff --git a/docs/content/getting-started/configuration-overview.md b/docs/content/getting-started/configuration-overview.md index 02a574ff7..c06a6bb0f 100644 --- a/docs/content/getting-started/configuration-overview.md +++ b/docs/content/getting-started/configuration-overview.md @@ -70,6 +70,12 @@ docker run traefik[:version] --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 All the configuration options are documented in their related section. diff --git a/pkg/cli/loader_env.go b/pkg/cli/loader_env.go index 3bff0251b..e1f015638 100644 --- a/pkg/cli/loader_env.go +++ b/pkg/cli/loader_env.go @@ -3,7 +3,6 @@ package cli import ( "fmt" "os" - "strings" "github.com/containous/traefik/pkg/config/env" "github.com/containous/traefik/pkg/log" @@ -14,23 +13,12 @@ type EnvLoader struct{} // Load loads the command's configuration from the environment variables. func (e *EnvLoader) Load(_ []string, cmd *Command) (bool, error) { - return e.load(os.Environ(), cmd) -} - -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 { + vars := env.FindPrefixedEnvVars(os.Environ(), env.DefaultNamePrefix, cmd.Configuration) + if len(vars) == 0 { 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) } diff --git a/pkg/config/env/env.go b/pkg/config/env/env.go index e71314b1d..d915272af 100644 --- a/pkg/config/env/env.go +++ b/pkg/config/env/env.go @@ -2,28 +2,38 @@ package env import ( + "fmt" + "regexp" "strings" "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. // The operation goes through four stages roughly summarized as: // env vars -> map // map -> tree of untyped nodes // untyped nodes -> nodes augmented with metadata such as kind (inferred from 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) for _, evr := range environ { 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]), "_", ".") 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. @@ -36,7 +46,7 @@ func Encode(element interface{}) ([]parser.Flat, error) { return nil, nil } - node, err := parser.EncodeToNode(element, false) + node, err := parser.EncodeToNode(element, parser.DefaultRootName, false) if err != nil { return nil, err } @@ -48,3 +58,17 @@ func Encode(element interface{}) ([]parser.Flat, error) { 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 +} diff --git a/pkg/config/env/env_test.go b/pkg/config/env/env_test.go index 342a1f77a..9294f4da6 100644 --- a/pkg/config/env/env_test.go +++ b/pkg/config/env/env_test.go @@ -173,7 +173,7 @@ func TestDecode(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - err := Decode(test.environ, test.element) + err := Decode(test.environ, DefaultNamePrefix, test.element) require.NoError(t, err) assert.Equal(t, test.expected, test.element) @@ -460,39 +460,3 @@ func TestEncode(t *testing.T) { 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 -} diff --git a/pkg/config/env/filter.go b/pkg/config/env/filter.go new file mode 100644 index 000000000..78604bc38 --- /dev/null +++ b/pkg/config/env/filter.go @@ -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 +} diff --git a/pkg/config/env/filter_test.go b/pkg/config/env/filter_test.go new file mode 100644 index 000000000..ecabc8982 --- /dev/null +++ b/pkg/config/env/filter_test.go @@ -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) + }) + } +} diff --git a/pkg/config/env/fixtures_test.go b/pkg/config/env/fixtures_test.go new file mode 100644 index 000000000..bbcb4c469 --- /dev/null +++ b/pkg/config/env/fixtures_test.go @@ -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 +} diff --git a/pkg/config/file/file_node.go b/pkg/config/file/file_node.go index d23e2344b..33534d922 100644 --- a/pkg/config/file/file_node.go +++ b/pkg/config/file/file_node.go @@ -36,13 +36,13 @@ func decodeFileToNode(filePath string, filters ...string) (*parser.Node, error) return nil, err } - return decodeRawToNode(data, filters...) + return decodeRawToNode(data, parser.DefaultRootName, filters...) default: return nil, fmt.Errorf("unsupported file extension: %s", filePath) } - return decodeRawToNode(data, filters...) + return decodeRawToNode(data, parser.DefaultRootName, filters...) } func getRootFieldNames(element interface{}) []string { diff --git a/pkg/config/file/raw_node.go b/pkg/config/file/raw_node.go index 8bcf776d2..f39499898 100644 --- a/pkg/config/file/raw_node.go +++ b/pkg/config/file/raw_node.go @@ -9,9 +9,9 @@ import ( "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{ - Name: "traefik", + Name: rootName, } vData := reflect.ValueOf(data) diff --git a/pkg/config/file/raw_node_test.go b/pkg/config/file/raw_node_test.go index 15cab5957..dd3fea861 100644 --- a/pkg/config/file/raw_node_test.go +++ b/pkg/config/file/raw_node_test.go @@ -531,7 +531,7 @@ func Test_decodeRawToNode(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - node, err := decodeRawToNode(test.data) + node, err := decodeRawToNode(test.data, parser.DefaultRootName) require.NoError(t, err) assert.Equal(t, test.expected, node) diff --git a/pkg/config/flag/flag.go b/pkg/config/flag/flag.go index e6e8f51e5..91a356f3b 100644 --- a/pkg/config/flag/flag.go +++ b/pkg/config/flag/flag.go @@ -17,7 +17,7 @@ func Decode(args []string, element interface{}) error { 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. @@ -30,7 +30,7 @@ func Encode(element interface{}) ([]parser.Flat, error) { return nil, nil } - node, err := parser.EncodeToNode(element, false) + node, err := parser.EncodeToNode(element, parser.DefaultRootName, false) if err != nil { return nil, err } diff --git a/pkg/config/flag/flagparser.go b/pkg/config/flag/flagparser.go index 3520e00c1..1009102cc 100644 --- a/pkg/config/flag/flagparser.go +++ b/pkg/config/flag/flagparser.go @@ -4,6 +4,8 @@ import ( "fmt" "reflect" "strings" + + "github.com/containous/traefik/pkg/config/parser" ) // 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) { - n := strings.ToLower("traefik." + name) + n := strings.ToLower(parser.DefaultRootName + "." + name) v, ok := f.values[n] if ok && f.flagTypes[name] == reflect.Slice { diff --git a/pkg/config/label/label.go b/pkg/config/label/label.go index e821e21aa..6b5478dee 100644 --- a/pkg/config/label/label.go +++ b/pkg/config/label/label.go @@ -13,7 +13,7 @@ func DecodeConfiguration(labels map[string]string) (*config.Configuration, error 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 { return nil, err } @@ -23,11 +23,11 @@ func DecodeConfiguration(labels map[string]string) (*config.Configuration, error // EncodeConfiguration converts a configuration to labels. 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. // labels -> [ node -> node + metadata (type) ] -> element (node) 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...) } diff --git a/pkg/config/parser/element_nodes.go b/pkg/config/parser/element_nodes.go index 3caabcce2..af0d431fa 100644 --- a/pkg/config/parser/element_nodes.go +++ b/pkg/config/parser/element_nodes.go @@ -9,9 +9,9 @@ import ( // EncodeToNode converts an element to a node. // element -> nodes -func EncodeToNode(element interface{}, omitEmpty bool) (*Node, error) { +func EncodeToNode(element interface{}, rootName string, omitEmpty bool) (*Node, error) { rValue := reflect.ValueOf(element) - node := &Node{Name: "traefik"} + node := &Node{Name: rootName} encoder := encoderToNode{omitEmpty: omitEmpty} diff --git a/pkg/config/parser/element_nodes_test.go b/pkg/config/parser/element_nodes_test.go index 854c50fa7..495b0518b 100644 --- a/pkg/config/parser/element_nodes_test.go +++ b/pkg/config/parser/element_nodes_test.go @@ -723,7 +723,7 @@ func TestEncodeToNode(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - node, err := EncodeToNode(test.element, true) + node, err := EncodeToNode(test.element, DefaultRootName, true) if test.expected.error { require.Error(t, err) diff --git a/pkg/config/parser/labels_decode.go b/pkg/config/parser/labels_decode.go index 13e560314..5f8f508e7 100644 --- a/pkg/config/parser/labels_decode.go +++ b/pkg/config/parser/labels_decode.go @@ -6,18 +6,16 @@ import ( "strings" ) -const labelRoot = "traefik" - // DecodeToNode converts the labels to a tree of nodes. // If any filters are present, labels which do not match the filters are skipped. -func DecodeToNode(labels map[string]string, filters ...string) (*Node, error) { +func DecodeToNode(labels map[string]string, rootName string, filters ...string) (*Node, error) { sortedKeys := sortKeys(labels, filters) var node *Node for i, key := range sortedKeys { split := strings.Split(key, ".") - if split[0] != labelRoot { + if split[0] != rootName { return nil, fmt.Errorf("invalid label root %s", split[0]) } diff --git a/pkg/config/parser/labels_decode_test.go b/pkg/config/parser/labels_decode_test.go index a6442b2a3..267265005 100644 --- a/pkg/config/parser/labels_decode_test.go +++ b/pkg/config/parser/labels_decode_test.go @@ -218,7 +218,7 @@ func TestDecodeToNode(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - out, err := DecodeToNode(test.in, test.filters...) + out, err := DecodeToNode(test.in, DefaultRootName, test.filters...) if test.expected.error { require.Error(t, err) diff --git a/pkg/config/parser/node.go b/pkg/config/parser/node.go index f756a0f07..88f0c7998 100644 --- a/pkg/config/parser/node.go +++ b/pkg/config/parser/node.go @@ -2,6 +2,9 @@ package parser 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. const MapNamePlaceholder = "" diff --git a/pkg/config/parser/parser.go b/pkg/config/parser/parser.go index e806ecc89..5258a5624 100644 --- a/pkg/config/parser/parser.go +++ b/pkg/config/parser/parser.go @@ -7,8 +7,8 @@ package parser // labels -> tree of untyped nodes // untyped nodes -> nodes augmented with metadata such as kind (inferred from element) // "typed" nodes -> typed element -func Decode(labels map[string]string, element interface{}, filters ...string) error { - node, err := DecodeToNode(labels, filters...) +func Decode(labels map[string]string, element interface{}, rootName string, filters ...string) error { + node, err := DecodeToNode(labels, rootName, filters...) if err != nil { return err } @@ -28,8 +28,8 @@ func Decode(labels map[string]string, element interface{}, filters ...string) er // Encode converts an element to labels. // element -> node (value) -> label (node) -func Encode(element interface{}) (map[string]string, error) { - node, err := EncodeToNode(element, true) +func Encode(element interface{}, rootName string) (map[string]string, error) { + node, err := EncodeToNode(element, rootName, true) if err != nil { return nil, err }