From ce6bbbaa3350d57b0516fa6f64388d994afb8760 Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sat, 2 Dec 2017 19:25:29 +0100 Subject: [PATCH] feat: labels/annotations parser. --- cmd/traefik/storeconfig.go | 2 +- provider/dynamodb/dynamodb.go | 8 +- provider/dynamodb/dynamodb_test.go | 8 +- provider/file/file.go | 8 +- provider/label/label.go | 273 ++++++++ provider/label/label_test.go | 969 +++++++++++++++++++++++++++++ provider/label/names.go | 105 ++++ server/server.go | 2 +- 8 files changed, 1361 insertions(+), 14 deletions(-) create mode 100644 provider/label/label.go create mode 100644 provider/label/label_test.go create mode 100644 provider/label/names.go diff --git a/cmd/traefik/storeconfig.go b/cmd/traefik/storeconfig.go index 534a0682d..2e1b1c7b2 100644 --- a/cmd/traefik/storeconfig.go +++ b/cmd/traefik/storeconfig.go @@ -56,7 +56,7 @@ func runStoreConfig(kv *staert.KvSource, traefikConfiguration *TraefikConfigurat } stdlog.Printf("Storing file configuration: %s\n", jsonConf) - config, err := fileConfig.LoadConfig() + config, err := fileConfig.BuildConfiguration() if err != nil { return err } diff --git a/provider/dynamodb/dynamodb.go b/provider/dynamodb/dynamodb.go index c021b598b..54898db6e 100644 --- a/provider/dynamodb/dynamodb.go +++ b/provider/dynamodb/dynamodb.go @@ -95,8 +95,8 @@ func (p *Provider) scanTable(client *dynamoClient) ([]map[string]*dynamodb.Attri return items, nil } -// loadDynamoConfig retrieves items from dynamodb and converts them into Backends and Frontends in a Configuration -func (p *Provider) loadDynamoConfig(client *dynamoClient) (*types.Configuration, error) { +// buildConfiguration retrieves items from dynamodb and converts them into Backends and Frontends in a Configuration +func (p *Provider) buildConfiguration(client *dynamoClient) (*types.Configuration, error) { items, err := p.scanTable(client) if err != nil { return nil, err @@ -167,7 +167,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s return handleCanceled(ctx, err) } - configuration, err := p.loadDynamoConfig(awsClient) + configuration, err := p.buildConfiguration(awsClient) if err != nil { return handleCanceled(ctx, err) } @@ -184,7 +184,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s log.Debug("Watching Provider...") select { case <-reload.C: - configuration, err := p.loadDynamoConfig(awsClient) + configuration, err := p.buildConfiguration(awsClient) if err != nil { return handleCanceled(ctx, err) } diff --git a/provider/dynamodb/dynamodb_test.go b/provider/dynamodb/dynamodb_test.go index 264036ba8..88878f7ef 100644 --- a/provider/dynamodb/dynamodb_test.go +++ b/provider/dynamodb/dynamodb_test.go @@ -84,14 +84,14 @@ func (m *mockDynamoDBClient) ScanPages(input *dynamodb.ScanInput, fn func(*dynam return nil } -func TestLoadDynamoConfigSuccessful(t *testing.T) { +func TestBuildConfigurationSuccessful(t *testing.T) { dbiface := &dynamoClient{ db: &mockDynamoDBClient{ testWithError: false, }, } provider := Provider{} - loadedConfig, err := provider.loadDynamoConfig(dbiface) + loadedConfig, err := provider.buildConfiguration(dbiface) if err != nil { t.Fatal(err) } @@ -108,14 +108,14 @@ func TestLoadDynamoConfigSuccessful(t *testing.T) { } } -func TestLoadDynamoConfigFailure(t *testing.T) { +func TestBuildConfigurationFailure(t *testing.T) { dbiface := &dynamoClient{ db: &mockDynamoDBClient{ testWithError: true, }, } provider := Provider{} - _, err := provider.loadDynamoConfig(dbiface) + _, err := provider.buildConfiguration(dbiface) if err == nil { t.Fatal("Expected error") } diff --git a/provider/file/file.go b/provider/file/file.go index 918c7cf4d..d56dca6e5 100644 --- a/provider/file/file.go +++ b/provider/file/file.go @@ -28,7 +28,7 @@ type Provider struct { // Provide allows the file provider to provide configurations to traefik // using the given configuration channel. func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { - configuration, err := p.LoadConfig() + configuration, err := p.BuildConfiguration() if err != nil { return err @@ -52,9 +52,9 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s return nil } -// LoadConfig loads configuration either from file or a directory specified by 'Filename'/'Directory' +// BuildConfiguration loads configuration either from file or a directory specified by 'Filename'/'Directory' // and returns a 'Configuration' object -func (p *Provider) LoadConfig() (*types.Configuration, error) { +func (p *Provider) BuildConfiguration() (*types.Configuration, error) { if p.Directory != "" { return loadFileConfigFromDirectory(p.Directory, nil) } @@ -108,7 +108,7 @@ func (p *Provider) watcherCallback(configurationChan chan<- types.ConfigMessage, return } - configuration, err := p.LoadConfig() + configuration, err := p.BuildConfiguration() if err != nil { log.Errorf("Error occurred during watcher callback: %s", err) diff --git a/provider/label/label.go b/provider/label/label.go new file mode 100644 index 000000000..12271269b --- /dev/null +++ b/provider/label/label.go @@ -0,0 +1,273 @@ +package label + +import ( + "fmt" + "net/http" + "regexp" + "strconv" + "strings" + + "github.com/containous/traefik/log" +) + +const ( + mapEntrySeparator = "||" + mapValueSeparator = ":" +) + +// Default values +const ( + DefaultWeight = "0" + DefaultProtocol = "http" + DefaultPassHostHeader = "true" + DefaultFrontendPriority = "0" + DefaultCircuitBreakerExpression = "NetworkErrorRatio() > 1" + DefaultFrontendRedirect = "" + DefaultBackendLoadBalancerMethod = "wrr" + DefaultBackendMaxconnExtractorFunc = "request.host" + DefaultBackendLoadbalancerStickinessCookieName = "" +) + +// ServicesPropertiesRegexp used to extract the name of the service and the name of the property for this service +// All properties are under the format traefik..frontend.*= except the port/portIndex/weight/protocol/backend directly after traefik.. +var ServicesPropertiesRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.(?Pport|portIndex|weight|protocol|backend|frontend\.(.+))$`) + +// PortRegexp used to extract the port label of the service +var PortRegexp = regexp.MustCompile(`^traefik\.(?P.+?)\.port$`) + +// ServicePropertyValues is a map of services properties +// an example value is: weight=42 +type ServicePropertyValues map[string]string + +// ServiceProperties is a map of service properties per service, +// which we can get with label[serviceName][propertyName]. +// It yields a property value. +type ServiceProperties map[string]ServicePropertyValues + +// GetStringValue get string value associated to a label +func GetStringValue(labels map[string]string, labelName string, defaultValue string) string { + if value, ok := labels[labelName]; ok && len(value) > 0 { + return value + } + return defaultValue +} + +// GetStringValueP get string value associated to a label +func GetStringValueP(labels *map[string]string, labelName string, defaultValue string) string { + if labels == nil { + return defaultValue + } + return GetStringValue(*labels, labelName, defaultValue) +} + +// GetBoolValue get bool value associated to a label +func GetBoolValue(labels map[string]string, labelName string, defaultValue bool) bool { + rawValue, ok := labels[labelName] + if ok { + v, err := strconv.ParseBool(rawValue) + if err == nil { + return v + } + } + return defaultValue +} + +// GetIntValue get int value associated to a label +func GetIntValue(labels map[string]string, labelName string, defaultValue int) int { + if rawValue, ok := labels[labelName]; ok { + value, err := strconv.Atoi(rawValue) + if err == nil { + return value + } + log.Errorf("Unable to parse %q: %q, falling back to %v. %v", labelName, rawValue, defaultValue, err) + } + return defaultValue +} + +// GetIntValueP get int value associated to a label +func GetIntValueP(labels *map[string]string, labelName string, defaultValue int) int { + if labels == nil { + return defaultValue + } + return GetIntValue(*labels, labelName, defaultValue) +} + +// GetInt64Value get int64 value associated to a label +func GetInt64Value(labels map[string]string, labelName string, defaultValue int64) int64 { + if rawValue, ok := labels[labelName]; ok { + value, err := strconv.ParseInt(rawValue, 10, 64) + if err == nil { + return value + } + log.Errorf("Unable to parse %q: %q, falling back to %v. %v", labelName, rawValue, defaultValue, err) + } + return defaultValue +} + +// GetInt64ValueP get int64 value associated to a label +func GetInt64ValueP(labels *map[string]string, labelName string, defaultValue int64) int64 { + if labels == nil { + return defaultValue + } + return GetInt64Value(*labels, labelName, defaultValue) +} + +// GetSliceStringValue get a slice of string associated to a label +func GetSliceStringValue(labels map[string]string, labelName string) []string { + var value []string + + if values, ok := labels[labelName]; ok { + value = SplitAndTrimString(values, ",") + + if len(value) == 0 { + log.Debugf("Could not load %q.", labelName) + } + } + return value +} + +// GetSliceStringValueP get a slice of string value associated to a label +func GetSliceStringValueP(labels *map[string]string, labelName string) []string { + if labels == nil { + return nil + } + return GetSliceStringValue(*labels, labelName) +} + +// GetMapValue get Map value associated to a label +func GetMapValue(labels map[string]string, labelName string) map[string]string { + if values, ok := labels[labelName]; ok { + + if len(values) == 0 { + log.Errorf("Missing value for %q, skipping...", labelName) + return nil + } + + mapValue := make(map[string]string) + + for _, parts := range strings.Split(values, mapEntrySeparator) { + pair := strings.SplitN(parts, mapValueSeparator, 2) + if len(pair) != 2 { + log.Warnf("Could not load %q: %q, skipping...", labelName, parts) + } else { + mapValue[http.CanonicalHeaderKey(strings.TrimSpace(pair[0]))] = strings.TrimSpace(pair[1]) + } + } + + if len(mapValue) == 0 { + log.Errorf("Could not load %q, skipping...", labelName) + return nil + } + return mapValue + } + + return nil +} + +// GetStringMultipleStrict get multiple string values associated to several labels +// Fail if one label is missing +func GetStringMultipleStrict(labels map[string]string, labelNames ...string) (map[string]string, error) { + foundLabels := map[string]string{} + for _, name := range labelNames { + value := GetStringValue(labels, name, "") + // Error out only if one of them is not defined. + if len(value) == 0 { + return nil, fmt.Errorf("label not found: %s", name) + } + foundLabels[name] = value + } + return foundLabels, nil +} + +// Has Check if a value is associated to a label +func Has(labels map[string]string, labelName string) bool { + value, ok := labels[labelName] + return ok && len(value) > 0 +} + +// HasP Check if a value is associated to a label +func HasP(labels *map[string]string, labelName string) bool { + if labels == nil { + return false + } + return Has(*labels, labelName) +} + +// ExtractServiceProperties Extract services labels +func ExtractServiceProperties(labels map[string]string) ServiceProperties { + v := make(ServiceProperties) + + for name, value := range labels { + matches := ServicesPropertiesRegexp.FindStringSubmatch(name) + if matches == nil { + continue + } + + var serviceName string + var propertyName string + for i, name := range ServicesPropertiesRegexp.SubexpNames() { + if i != 0 { + if name == "service_name" { + serviceName = matches[i] + } else if name == "property_name" { + propertyName = matches[i] + } + } + } + + if _, ok := v[serviceName]; !ok { + v[serviceName] = make(ServicePropertyValues) + } + v[serviceName][propertyName] = value + } + + return v +} + +// ExtractServicePropertiesP Extract services labels +func ExtractServicePropertiesP(labels *map[string]string) ServiceProperties { + if labels == nil { + return make(ServiceProperties) + } + return ExtractServiceProperties(*labels) +} + +// IsEnabled Check if a container is enabled in Træfik +func IsEnabled(labels map[string]string, exposedByDefault bool) bool { + return GetBoolValue(labels, TraefikEnable, exposedByDefault) +} + +// IsEnabledP Check if a container is enabled in Træfik +func IsEnabledP(labels *map[string]string, exposedByDefault bool) bool { + if labels == nil { + return exposedByDefault + } + return IsEnabled(*labels, exposedByDefault) +} + +// SplitAndTrimString splits separatedString at the separator character and trims each +// piece, filtering out empty pieces. Returns the list of pieces or nil if the input +// did not contain a non-empty piece. +func SplitAndTrimString(base string, sep string) []string { + var trimmedStrings []string + + for _, s := range strings.Split(base, sep) { + s = strings.TrimSpace(s) + if len(s) > 0 { + trimmedStrings = append(trimmedStrings, s) + } + } + + return trimmedStrings +} + +// GetServiceLabel converts a key value of Label*, given a serviceName, +// into a pattern .. +// i.e. For LabelFrontendRule and serviceName=app it will return "traefik.app.frontend.rule" +func GetServiceLabel(labelName, serviceName string) string { + if len(serviceName) > 0 { + property := strings.TrimPrefix(labelName, Prefix) + return Prefix + serviceName + "." + property + } + return labelName +} diff --git a/provider/label/label_test.go b/provider/label/label_test.go new file mode 100644 index 000000000..3347ddb11 --- /dev/null +++ b/provider/label/label_test.go @@ -0,0 +1,969 @@ +package label + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSplitAndTrimString(t *testing.T) { + testCases := []struct { + desc string + input string + expected []string + }{ + { + desc: "empty string", + input: "", + expected: nil, + }, { + desc: "one piece", + input: "foo", + expected: []string{"foo"}, + }, { + desc: "two pieces", + input: "foo,bar", + expected: []string{"foo", "bar"}, + }, { + desc: "three pieces", + input: "foo,bar,zoo", + expected: []string{"foo", "bar", "zoo"}, + }, { + desc: "two pieces with whitespace", + input: " foo , bar ", + expected: []string{"foo", "bar"}, + }, { + desc: "consecutive commas", + input: " foo ,, bar ", + expected: []string{"foo", "bar"}, + }, { + desc: "consecutive commas with whitespace", + input: " foo , , bar ", + expected: []string{"foo", "bar"}, + }, { + desc: "leading and trailing commas", + input: ",, foo , , bar,, , ", + expected: []string{"foo", "bar"}, + }, { + desc: "no valid pieces", + input: ", , , ,, ,", + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + actual := SplitAndTrimString(test.input, ",") + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetStringValue(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + labelName string + defaultValue string + expected string + }{ + { + desc: "empty labels map", + labelName: "foo", + defaultValue: "default", + expected: "default", + }, + { + desc: "existing label", + labels: map[string]string{ + "foo": "bar", + }, + labelName: "foo", + defaultValue: "default", + expected: "bar", + }, + { + desc: "non existing label", + labels: map[string]string{ + "foo": "bar", + }, + labelName: "fii", + defaultValue: "default", + expected: "default", + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetStringValue(test.labels, test.labelName, test.defaultValue) + assert.Equal(t, test.expected, got) + }) + } +} + +func TestGetStringValueP(t *testing.T) { + testCases := []struct { + desc string + labels *map[string]string + labelName string + defaultValue string + expected string + }{ + { + desc: "nil labels map", + labels: nil, + labelName: "foo", + defaultValue: "default", + expected: "default", + }, + { + desc: "existing label", + labels: &map[string]string{ + "foo": "bar", + }, + labelName: "foo", + defaultValue: "default", + expected: "bar", + }, + { + desc: "non existing label", + labels: &map[string]string{ + "foo": "bar", + }, + labelName: "fii", + defaultValue: "default", + expected: "default", + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetStringValueP(test.labels, test.labelName, test.defaultValue) + assert.Equal(t, test.expected, got) + }) + } +} + +func TestGetBoolValue(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + labelName string + defaultValue bool + expected bool + }{ + { + desc: "empty map", + labelName: "foo", + }, + { + desc: "invalid boolean value", + labels: map[string]string{ + "foo": "bar", + }, + labelName: "foo", + defaultValue: true, + expected: true, + }, + { + desc: "valid boolean value: true", + labels: map[string]string{ + "foo": "true", + }, + labelName: "foo", + defaultValue: false, + expected: true, + }, + { + desc: "valid boolean value: false", + labels: map[string]string{ + "foo": "false", + }, + labelName: "foo", + defaultValue: true, + expected: false, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetBoolValue(test.labels, test.labelName, test.defaultValue) + assert.Equal(t, test.expected, got) + }) + } +} + +func TestGetIntValue(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + labelName string + defaultValue int + expected int + }{ + { + desc: "empty map", + labelName: "foo", + }, + { + desc: "invalid int value", + labelName: "foo", + labels: map[string]string{ + "foo": "bar", + }, + defaultValue: 666, + expected: 666, + }, + { + desc: "negative int value", + labelName: "foo", + labels: map[string]string{ + "foo": "-1", + }, + defaultValue: 666, + expected: -1, + }, + { + desc: "positive int value", + labelName: "foo", + labels: map[string]string{ + "foo": "1", + }, + defaultValue: 666, + expected: 1, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetIntValue(test.labels, test.labelName, test.defaultValue) + assert.Equal(t, test.expected, got) + }) + } +} + +func TestGetIntValueP(t *testing.T) { + testCases := []struct { + desc string + labels *map[string]string + labelName string + defaultValue int + expected int + }{ + { + desc: "nil map", + labels: nil, + labelName: "foo", + defaultValue: 666, + expected: 666, + }, + { + desc: "invalid int value", + labelName: "foo", + labels: &map[string]string{ + "foo": "bar", + }, + defaultValue: 666, + expected: 666, + }, + { + desc: "negative int value", + labelName: "foo", + labels: &map[string]string{ + "foo": "-1", + }, + defaultValue: 666, + expected: -1, + }, + { + desc: "positive int value", + labelName: "foo", + labels: &map[string]string{ + "foo": "1", + }, + defaultValue: 666, + expected: 1, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetIntValueP(test.labels, test.labelName, test.defaultValue) + assert.Equal(t, test.expected, got) + }) + } +} + +func TestGetInt64Value(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + labelName string + defaultValue int64 + expected int64 + }{ + { + desc: "empty map", + labelName: "foo", + }, + { + desc: "invalid int value", + labelName: "foo", + labels: map[string]string{ + "foo": "bar", + }, + defaultValue: 666, + expected: 666, + }, + { + desc: "negative int value", + labelName: "foo", + labels: map[string]string{ + "foo": "-1", + }, + defaultValue: 666, + expected: -1, + }, + { + desc: "positive int value", + labelName: "foo", + labels: map[string]string{ + "foo": "1", + }, + defaultValue: 666, + expected: 1, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetInt64Value(test.labels, test.labelName, test.defaultValue) + assert.Equal(t, test.expected, got) + }) + } +} + +func TestGetInt64ValueP(t *testing.T) { + testCases := []struct { + desc string + labels *map[string]string + labelName string + defaultValue int64 + expected int64 + }{ + { + desc: "nil map", + labels: nil, + labelName: "foo", + defaultValue: 666, + expected: 666, + }, + { + desc: "invalid int value", + labelName: "foo", + labels: &map[string]string{ + "foo": "bar", + }, + defaultValue: 666, + expected: 666, + }, + { + desc: "negative int value", + labelName: "foo", + labels: &map[string]string{ + "foo": "-1", + }, + defaultValue: 666, + expected: -1, + }, + { + desc: "positive int value", + labelName: "foo", + labels: &map[string]string{ + "foo": "1", + }, + defaultValue: 666, + expected: 1, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetInt64ValueP(test.labels, test.labelName, test.defaultValue) + assert.Equal(t, test.expected, got) + }) + } +} + +func TestGetSliceStringValue(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + labelName string + expected []string + }{ + { + desc: "empty map", + labelName: "foo", + }, + { + desc: "empty value", + labels: map[string]string{ + "foo": "", + }, + labelName: "foo", + expected: nil, + }, + { + desc: "one value, not split", + labels: map[string]string{ + "foo": "bar", + }, + labelName: "foo", + expected: []string{"bar"}, + }, + { + desc: "several values", + labels: map[string]string{ + "foo": "bar,bir ,bur", + }, + labelName: "foo", + expected: []string{"bar", "bir", "bur"}, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetSliceStringValue(test.labels, test.labelName) + assert.EqualValues(t, test.expected, got) + }) + } +} + +func TestGetSliceStringValueP(t *testing.T) { + testCases := []struct { + desc string + labels *map[string]string + labelName string + expected []string + }{ + { + desc: "nil map", + labels: nil, + labelName: "foo", + expected: nil, + }, + { + desc: "one value, not split", + labels: &map[string]string{ + "foo": "bar", + }, + labelName: "foo", + expected: []string{"bar"}, + }, + { + desc: "several values", + labels: &map[string]string{ + "foo": "bar,bir ,bur", + }, + labelName: "foo", + expected: []string{"bar", "bir", "bur"}, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetSliceStringValueP(test.labels, test.labelName) + assert.EqualValues(t, test.expected, got) + }) + } +} + +func TestGetMapValue(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + labelName string + expected map[string]string + }{ + { + desc: "empty map", + labelName: "foo", + }, + { + desc: "existent label with empty entry", + labelName: "foo", + labels: map[string]string{ + "foo": "", + }, + expected: nil, + }, + { + desc: "existent label with invalid entry", + labelName: "foo", + labels: map[string]string{ + "foo": "bar", + }, + expected: nil, + }, + { + desc: "existent label with empty value", + labelName: "foo", + labels: map[string]string{ + "foo": "bar:", + }, + expected: map[string]string{ + "Bar": "", + }, + }, + { + desc: "one entry", + labelName: "foo", + labels: map[string]string{ + "foo": " Access-Control-Allow-Methods:POST,GET,OPTIONS ", + }, + expected: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + }, + }, + { + desc: "several entry", + labelName: "foo", + labels: map[string]string{ + "foo": "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + }, + expected: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetMapValue(test.labels, test.labelName) + assert.EqualValues(t, test.expected, got) + }) + } +} + +func TestGetStringMultipleStrict(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + labelNames []string + expected map[string]string + expectedErr bool + }{ + { + desc: "empty labels names and empty labels map", + labels: map[string]string{}, + expected: map[string]string{}, + }, + { + desc: "empty labels names", + labels: map[string]string{ + "foo": "bar", + "fii": "bir", + }, + expected: map[string]string{}, + }, + { + desc: "one label missing", + labels: map[string]string{ + "foo": "bar", + "fii": "bir", + "fyy": "byr", + }, + labelNames: []string{"foo", "fii", "fuu"}, + expected: nil, + expectedErr: true, + }, + { + desc: "all labels are present", + labels: map[string]string{ + "foo": "bar", + "fii": "bir", + "fyy": "byr", + }, + labelNames: []string{"foo", "fii"}, + expected: map[string]string{ + "foo": "bar", + "fii": "bir", + }, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got, err := GetStringMultipleStrict(test.labels, test.labelNames...) + if (err != nil) != test.expectedErr { + t.Errorf("error = %v, wantErr %v", err, test.expectedErr) + return + } + assert.EqualValues(t, test.expected, got) + }) + } +} + +func TestHas(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + labelName string + expected bool + }{ + { + desc: "nil labels map", + labelName: "foo", + }, + { + desc: "nonexistent label", + labels: map[string]string{ + "foo": "bar", + }, + labelName: "fii", + expected: false, + }, + { + desc: "existent label", + labels: map[string]string{ + "foo": "bar", + }, + labelName: "foo", + expected: true, + }, + { + desc: "existent label with empty value", + labels: map[string]string{ + "foo": "", + }, + labelName: "foo", + expected: false, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := Has(test.labels, test.labelName) + assert.Equal(t, test.expected, got) + }) + } +} + +func TestHasP(t *testing.T) { + testCases := []struct { + desc string + labels *map[string]string + labelName string + expected bool + }{ + { + desc: "nil labels map", + labelName: "foo", + }, + { + desc: "nonexistent label", + labels: &map[string]string{ + "foo": "bar", + }, + labelName: "fii", + expected: false, + }, + { + desc: "existent label", + labels: &map[string]string{ + "foo": "bar", + }, + labelName: "foo", + expected: true, + }, + { + desc: "existent label with empty value", + labels: &map[string]string{ + "foo": "", + }, + labelName: "foo", + expected: false, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := HasP(test.labels, test.labelName) + assert.Equal(t, test.expected, got) + }) + } +} + +func TestExtractServiceProperties(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + expected ServiceProperties + }{ + { + desc: "empty labels map", + expected: ServiceProperties{}, + }, + { + desc: "valid label names", + labels: map[string]string{ + "traefik.foo.port": "bar", + "traefik.foo.frontend.bar": "1bar", + "traefik.foo.frontend.": "2bar", + }, + expected: ServiceProperties{ + "foo": ServicePropertyValues{ + "port": "bar", + "frontend.bar": "1bar", + }, + }, + }, + { + desc: "invalid label names", + labels: map[string]string{ + "foo.frontend.bar": "1bar", + "traefik.foo.frontend.": "2bar", + "traefik.foo.port.bar": "barbar", + "traefik.foo.frontend": "0bar", + }, + expected: ServiceProperties{}, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := ExtractServiceProperties(test.labels) + assert.EqualValues(t, test.expected, got) + }) + } +} + +func TestExtractServicePropertiesP(t *testing.T) { + testCases := []struct { + desc string + labels *map[string]string + expected ServiceProperties + }{ + { + desc: "nil labels map", + expected: ServiceProperties{}, + }, + { + desc: "valid label names", + labels: &map[string]string{ + "traefik.foo.port": "bar", + "traefik.foo.frontend.bar": "1bar", + "traefik.foo.frontend.": "2bar", + }, + expected: ServiceProperties{ + "foo": ServicePropertyValues{ + "port": "bar", + "frontend.bar": "1bar", + }, + }, + }, + { + desc: "invalid label names", + labels: &map[string]string{ + "foo.frontend.bar": "1bar", + "traefik.foo.frontend.": "2bar", + "traefik.foo.port.bar": "barbar", + "traefik.foo.frontend": "0bar", + }, + expected: ServiceProperties{}, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := ExtractServicePropertiesP(test.labels) + assert.EqualValues(t, test.expected, got) + }) + } +} + +func TestIsEnabled(t *testing.T) { + testCases := []struct { + desc string + labels map[string]string + exposedByDefault bool + expected bool + }{ + { + desc: "empty labels map & exposedByDefault true", + exposedByDefault: true, + expected: true, + }, + { + desc: "empty labels map & exposedByDefault false", + exposedByDefault: false, + expected: false, + }, + { + desc: "exposedByDefault false and label enable true", + labels: map[string]string{ + TraefikEnable: "true", + }, + exposedByDefault: false, + expected: true, + }, + { + desc: "exposedByDefault false and label enable false", + labels: map[string]string{ + TraefikEnable: "false", + }, + exposedByDefault: false, + expected: false, + }, + { + desc: "exposedByDefault true and label enable false", + labels: map[string]string{ + TraefikEnable: "false", + }, + exposedByDefault: true, + expected: false, + }, + { + desc: "exposedByDefault true and label enable true", + labels: map[string]string{ + TraefikEnable: "true", + }, + exposedByDefault: true, + expected: true, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := IsEnabled(test.labels, test.exposedByDefault) + assert.Equal(t, test.expected, got) + }) + } +} + +func TestIsEnabledP(t *testing.T) { + testCases := []struct { + desc string + labels *map[string]string + exposedByDefault bool + expected bool + }{ + { + desc: "nil labels map & exposedByDefault true", + exposedByDefault: true, + expected: true, + }, + { + desc: "nil labels map & exposedByDefault false", + exposedByDefault: false, + expected: false, + }, + { + desc: "exposedByDefault false and label enable true", + labels: &map[string]string{ + TraefikEnable: "true", + }, + exposedByDefault: false, + expected: true, + }, + { + desc: "exposedByDefault false and label enable false", + labels: &map[string]string{ + TraefikEnable: "false", + }, + exposedByDefault: false, + expected: false, + }, + { + desc: "exposedByDefault true and label enable false", + labels: &map[string]string{ + TraefikEnable: "false", + }, + exposedByDefault: true, + expected: false, + }, + { + desc: "exposedByDefault true and label enable true", + labels: &map[string]string{ + TraefikEnable: "true", + }, + exposedByDefault: true, + expected: true, + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := IsEnabledP(test.labels, test.exposedByDefault) + assert.Equal(t, test.expected, got) + }) + } +} + +func TestGetServiceLabel(t *testing.T) { + testCases := []struct { + desc string + labelName string + serviceName string + expected string + }{ + { + desc: "without service name", + labelName: TraefikPort, + expected: TraefikPort, + }, + { + desc: "with service name", + labelName: TraefikPort, + serviceName: "bar", + expected: "traefik.bar.port", + }, + } + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + got := GetServiceLabel(test.labelName, test.serviceName) + assert.Equal(t, test.expected, got) + }) + } +} diff --git a/provider/label/names.go b/provider/label/names.go new file mode 100644 index 000000000..4c356ae97 --- /dev/null +++ b/provider/label/names.go @@ -0,0 +1,105 @@ +package label + +// Traefik labels +const ( + Prefix = "traefik." + SuffixBackend = "backend" + SuffixDomain = "domain" + SuffixEnable = "enable" + SuffixPort = "port" + SuffixPortIndex = "portIndex" + SuffixProtocol = "protocol" + SuffixTags = "tags" + SuffixWeight = "weight" + SuffixBackendID = "backend.id" + SuffixBackendCircuitBreaker = "backend.circuitbreaker" + SuffixBackendCircuitBreakerExpression = "backend.circuitbreaker.expression" + SuffixBackendHealthCheckPath = "backend.healthcheck.path" + SuffixBackendHealthCheckInterval = "backend.healthcheck.interval" + SuffixBackendLoadBalancerMethod = "backend.loadbalancer.method" + SuffixBackendLoadBalancerSticky = "backend.loadbalancer.sticky" + SuffixBackendLoadBalancerStickiness = "backend.loadbalancer.stickiness" + SuffixBackendLoadBalancerStickinessCookieName = "backend.loadbalancer.stickiness.cookieName" + SuffixBackendMaxConnAmount = "backend.maxconn.amount" + SuffixBackendMaxConnExtractorFunc = "backend.maxconn.extractorfunc" + SuffixFrontendAuthBasic = "frontend.auth.basic" + SuffixFrontendBackend = "frontend.backend" + SuffixFrontendEntryPoints = "frontend.entryPoints" + SuffixFrontendRequestHeaders = "frontend.headers.customRequestHeaders" + SuffixFrontendResponseHeaders = "frontend.headers.customResponseHeaders" + SuffixFrontendHeadersAllowedHosts = "frontend.headers.allowedHosts" + SuffixFrontendHeadersHostsProxyHeaders = "frontend.headers.hostsProxyHeaders" + SuffixFrontendHeadersSSLRedirect = "frontend.headers.SSLRedirect" + SuffixFrontendHeadersSSLTemporaryRedirect = "frontend.headers.SSLTemporaryRedirect" + SuffixFrontendHeadersSSLHost = "frontend.headers.SSLHost" + SuffixFrontendHeadersSSLProxyHeaders = "frontend.headers.SSLProxyHeaders" + SuffixFrontendHeadersSTSSeconds = "frontend.headers.STSSeconds" + SuffixFrontendHeadersSTSIncludeSubdomains = "frontend.headers.STSIncludeSubdomains" + SuffixFrontendHeadersSTSPreload = "frontend.headers.STSPreload" + SuffixFrontendHeadersForceSTSHeader = "frontend.headers.forceSTSHeader" + SuffixFrontendHeadersFrameDeny = "frontend.headers.frameDeny" + SuffixFrontendHeadersCustomFrameOptionsValue = "frontend.headers.customFrameOptionsValue" + SuffixFrontendHeadersContentTypeNosniff = "frontend.headers.contentTypeNosniff" + SuffixFrontendHeadersBrowserXSSFilter = "frontend.headers.browserXSSFilter" + SuffixFrontendHeadersContentSecurityPolicy = "frontend.headers.contentSecurityPolicy" + SuffixFrontendHeadersPublicKey = "frontend.headers.publicKey" + SuffixFrontendHeadersReferrerPolicy = "frontend.headers.referrerPolicy" + SuffixFrontendHeadersIsDevelopment = "frontend.headers.isDevelopment" + SuffixFrontendPassHostHeader = "frontend.passHostHeader" + SuffixFrontendPassTLSCert = "frontend.passTLSCert" + SuffixFrontendPriority = "frontend.priority" + SuffixFrontendRedirect = "frontend.redirect" + SuffixFrontendRule = "frontend.rule" + SuffixFrontendRuleType = "frontend.rule.type" + SuffixFrontendWhitelistSourceRange = "frontend.whitelistSourceRange" + SuffixFrontendValue = "frontend.value" + TraefikDomain = Prefix + SuffixDomain + TraefikEnable = Prefix + SuffixEnable + TraefikPort = Prefix + SuffixPort + TraefikPortIndex = Prefix + SuffixPortIndex + TraefikProtocol = Prefix + SuffixProtocol + TraefikTags = Prefix + SuffixTags + TraefikWeight = Prefix + SuffixWeight + TraefikBackend = Prefix + SuffixBackend + TraefikBackendID = Prefix + SuffixBackendID + TraefikBackendCircuitBreaker = Prefix + SuffixBackendCircuitBreaker + TraefikBackendCircuitBreakerExpression = Prefix + SuffixBackendCircuitBreakerExpression + TraefikBackendHealthCheckPath = Prefix + SuffixBackendHealthCheckPath + TraefikBackendHealthCheckInterval = Prefix + SuffixBackendHealthCheckInterval + TraefikBackendLoadBalancerMethod = Prefix + SuffixBackendLoadBalancerMethod + TraefikBackendLoadBalancerSticky = Prefix + SuffixBackendLoadBalancerSticky + TraefikBackendLoadBalancerStickiness = Prefix + SuffixBackendLoadBalancerStickiness + TraefikBackendLoadBalancerStickinessCookieName = Prefix + SuffixBackendLoadBalancerStickinessCookieName + TraefikBackendMaxConnAmount = Prefix + SuffixBackendMaxConnAmount + TraefikBackendMaxConnExtractorFunc = Prefix + SuffixBackendMaxConnExtractorFunc + TraefikFrontendAuthBasic = Prefix + SuffixFrontendAuthBasic + TraefikFrontendEntryPoints = Prefix + SuffixFrontendEntryPoints + TraefikFrontendPassHostHeader = Prefix + SuffixFrontendPassHostHeader + TraefikFrontendPassTLSCert = Prefix + SuffixFrontendPassTLSCert + TraefikFrontendPriority = Prefix + SuffixFrontendPriority + TraefikFrontendRule = Prefix + SuffixFrontendRule + TraefikFrontendRuleType = Prefix + SuffixFrontendRuleType + TraefikFrontendRedirect = Prefix + SuffixFrontendRedirect + TraefikFrontendValue = Prefix + SuffixFrontendValue + TraefikFrontendWhitelistSourceRange = Prefix + SuffixFrontendWhitelistSourceRange + TraefikFrontendRequestHeaders = Prefix + SuffixFrontendRequestHeaders + TraefikFrontendResponseHeaders = Prefix + SuffixFrontendResponseHeaders + TraefikFrontendAllowedHosts = Prefix + SuffixFrontendHeadersAllowedHosts + TraefikFrontendHostsProxyHeaders = Prefix + SuffixFrontendHeadersHostsProxyHeaders + TraefikFrontendSSLRedirect = Prefix + SuffixFrontendHeadersSSLRedirect + TraefikFrontendSSLTemporaryRedirect = Prefix + SuffixFrontendHeadersSSLTemporaryRedirect + TraefikFrontendSSLHost = Prefix + SuffixFrontendHeadersSSLHost + TraefikFrontendSSLProxyHeaders = Prefix + SuffixFrontendHeadersSSLProxyHeaders + TraefikFrontendSTSSeconds = Prefix + SuffixFrontendHeadersSTSSeconds + TraefikFrontendSTSIncludeSubdomains = Prefix + SuffixFrontendHeadersSTSIncludeSubdomains + TraefikFrontendSTSPreload = Prefix + SuffixFrontendHeadersSTSPreload + TraefikFrontendForceSTSHeader = Prefix + SuffixFrontendHeadersForceSTSHeader + TraefikFrontendFrameDeny = Prefix + SuffixFrontendHeadersFrameDeny + TraefikFrontendCustomFrameOptionsValue = Prefix + SuffixFrontendHeadersCustomFrameOptionsValue + TraefikFrontendContentTypeNosniff = Prefix + SuffixFrontendHeadersContentTypeNosniff + TraefikFrontendBrowserXSSFilter = Prefix + SuffixFrontendHeadersBrowserXSSFilter + TraefikFrontendContentSecurityPolicy = Prefix + SuffixFrontendHeadersContentSecurityPolicy + TraefikFrontendPublicKey = Prefix + SuffixFrontendHeadersPublicKey + TraefikFrontendReferrerPolicy = Prefix + SuffixFrontendHeadersReferrerPolicy + TraefikFrontendIsDevelopment = Prefix + SuffixFrontendHeadersIsDevelopment +) diff --git a/server/server.go b/server/server.go index e2255131a..21235fec0 100644 --- a/server/server.go +++ b/server/server.go @@ -888,7 +888,7 @@ func (s *Server) getRoundTripper(entryPointName string, globalConfiguration conf return s.defaultForwardingRoundTripper, nil } -// LoadConfig returns a new gorilla.mux Route from the specified global configuration and the dynamic +// loadConfig returns a new gorilla.mux Route from the specified global configuration and the dynamic // provider configurations. func (s *Server) loadConfig(configurations types.Configurations, globalConfiguration configuration.GlobalConfiguration) (map[string]*serverEntryPoint, error) { serverEntryPoints := s.buildEntryPoints(globalConfiguration)