diff --git a/integration/etcd3_test.go b/integration/etcd3_test.go index 24af22f30..58a622ead 100644 --- a/integration/etcd3_test.go +++ b/integration/etcd3_test.go @@ -19,14 +19,14 @@ import ( const ( // Services IP addresses fixed in the configuration - ipEtcd string = "172.18.0.2" - ipWhoami01 string = "172.18.0.3" - ipWhoami02 string = "172.18.0.4" - ipWhoami03 string = "172.18.0.5" - ipWhoami04 string = "172.18.0.6" + ipEtcd = "172.18.0.2" + ipWhoami01 = "172.18.0.3" + ipWhoami02 = "172.18.0.4" + ipWhoami03 = "172.18.0.5" + ipWhoami04 = "172.18.0.6" - traefikEtcdURL string = "http://127.0.0.1:8000/" - traefikWebEtcdURL string = "http://127.0.0.1:8081/" + traefikEtcdURL = "http://127.0.0.1:8000/" + traefikWebEtcdURL = "http://127.0.0.1:8081/" ) // Etcd test suites (using libcompose) @@ -289,7 +289,7 @@ func (s *Etcd3Suite) TestGlobalConfiguration(c *check.C) { c.Assert(err, checker.IsNil) } -func (s *Etcd3Suite) TestCertificatesContentstWithSNIConfigHandshake(c *check.C) { +func (s *Etcd3Suite) TestCertificatesContentWithSNIConfigHandshake(c *check.C) { // start traefik cmd, display := s.traefikCmd( withConfigFile("fixtures/simple_web.toml"), diff --git a/integration/etcd_test.go b/integration/etcd_test.go index e435da7dc..24d57884b 100644 --- a/integration/etcd_test.go +++ b/integration/etcd_test.go @@ -291,7 +291,7 @@ func (s *EtcdSuite) TestGlobalConfiguration(c *check.C) { c.Assert(err, checker.IsNil) } -func (s *EtcdSuite) TestCertificatesContentstWithSNIConfigHandshake(c *check.C) { +func (s *EtcdSuite) TestCertificatesContentWithSNIConfigHandshake(c *check.C) { etcdHost := s.composeProject.Container(c, "etcd").NetworkSettings.IPAddress // start Træfik cmd, display := s.traefikCmd( diff --git a/provider/kv/filler_test.go b/provider/kv/filler_test.go new file mode 100644 index 000000000..22505756d --- /dev/null +++ b/provider/kv/filler_test.go @@ -0,0 +1,121 @@ +package kv + +import ( + "sort" + "strings" + "testing" + + "github.com/docker/libkv/store" + "github.com/stretchr/testify/assert" +) + +type ByKey []*store.KVPair + +func (a ByKey) Len() int { return len(a) } +func (a ByKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a ByKey) Less(i, j int) bool { return a[i].Key < a[j].Key } + +func filler(prefix string, opts ...func(string, map[string]*store.KVPair)) []*store.KVPair { + buf := make(map[string]*store.KVPair) + for _, opt := range opts { + opt(prefix, buf) + } + + var result ByKey + for _, value := range buf { + result = append(result, value) + } + + sort.Sort(result) + return result +} + +func backend(name string, opts ...func(map[string]string)) func(string, map[string]*store.KVPair) { + return entry(pathBackends+name, opts...) +} + +func frontend(name string, opts ...func(map[string]string)) func(string, map[string]*store.KVPair) { + return entry(pathFrontends+name, opts...) +} + +func entry(root string, opts ...func(map[string]string)) func(string, map[string]*store.KVPair) { + return func(prefix string, pairs map[string]*store.KVPair) { + prefixedRoot := prefix + pathSeparator + strings.TrimPrefix(root, pathSeparator) + pairs[prefixedRoot] = &store.KVPair{Key: prefixedRoot, Value: []byte("")} + + transit := make(map[string]string) + for _, opt := range opts { + opt(transit) + } + + for key, value := range transit { + fill(pairs, prefixedRoot, key, value) + } + } +} + +func fill(pairs map[string]*store.KVPair, previous string, current string, value string) { + clean := strings.TrimPrefix(current, pathSeparator) + + i := strings.IndexRune(clean, '/') + if i > 0 { + key := previous + pathSeparator + clean[:i] + + if _, ok := pairs[key]; !ok || len(pairs[key].Value) == 0 { + pairs[key] = &store.KVPair{Key: key, Value: []byte("")} + } + + fill(pairs, key, clean[i:], value) + } + + key := previous + pathSeparator + clean + pairs[key] = &store.KVPair{Key: key, Value: []byte(value)} +} + +func withPair(key string, value string) func(map[string]string) { + return func(pairs map[string]string) { + if len(key) == 0 { + return + } + pairs[key] = value + } +} + +func TestFiller(t *testing.T) { + expected := []*store.KVPair{ + {Key: "traefik/backends/backend.with.dot.too", Value: []byte("")}, + {Key: "traefik/backends/backend.with.dot.too/servers", Value: []byte("")}, + {Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot", Value: []byte("")}, + {Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot.without.url", Value: []byte("")}, + {Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot.without.url/weight", Value: []byte("0")}, + {Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot/url", Value: []byte("http://172.17.0.2:80")}, + {Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot/weight", Value: []byte("0")}, + {Key: "traefik/frontends/frontend.with.dot", Value: []byte("")}, + {Key: "traefik/frontends/frontend.with.dot/backend", Value: []byte("backend.with.dot.too")}, + {Key: "traefik/frontends/frontend.with.dot/routes", Value: []byte("")}, + {Key: "traefik/frontends/frontend.with.dot/routes/route.with.dot", Value: []byte("")}, + {Key: "traefik/frontends/frontend.with.dot/routes/route.with.dot/rule", Value: []byte("Host:test.localhost")}, + } + + pairs1 := filler("traefik", + frontend("frontend.with.dot", + withPair("backend", "backend.with.dot.too"), + withPair("routes/route.with.dot/rule", "Host:test.localhost")), + backend("backend.with.dot.too", + withPair("servers/server.with.dot/url", "http://172.17.0.2:80"), + withPair("servers/server.with.dot/weight", "0"), + withPair("servers/server.with.dot.without.url/weight", "0")), + ) + assert.EqualValues(t, expected, pairs1) + + pairs2 := filler("traefik", + entry("frontends/frontend.with.dot", + withPair("backend", "backend.with.dot.too"), + withPair("routes/route.with.dot/rule", "Host:test.localhost")), + entry("backends/backend.with.dot.too", + withPair("servers/server.with.dot/url", "http://172.17.0.2:80"), + withPair("servers/server.with.dot/weight", "0"), + withPair("servers/server.with.dot.without.url/weight", "0")), + ) + assert.EqualValues(t, expected, pairs2) +} diff --git a/provider/kv/keynames.go b/provider/kv/keynames.go new file mode 100644 index 000000000..f2217c986 --- /dev/null +++ b/provider/kv/keynames.go @@ -0,0 +1,24 @@ +package kv + +const ( + pathBackends = "/backends/" + pathBackendCircuitBreakerExpression = "/circuitbreaker/expression" + pathBackendHealthCheckPath = "/healthcheck/path" + pathBackendHealthCheckInterval = "/healthcheck/interval" + pathBackendLoadBalancerMethod = "/loadbalancer/method" + pathBackendLoadBalancerSticky = "/loadbalancer/sticky" + pathBackendLoadBalancerStickiness = "/loadbalancer/stickiness" + pathBackendLoadBalancerStickinessCookieName = "/loadbalancer/stickiness/cookiename" + pathBackendServers = "/servers/" + pathBackendServerURL = "/url" + + pathFrontends = "/frontends/" + pathFrontendBackend = "/backend" + pathFrontendPriority = "/priority" + pathFrontendPassHostHeader = "/passHostHeader" + pathFrontendEntryPoints = "/entrypoints" + + pathTags = "/tags" + pathAlias = "/alias" + pathSeparator = "/" +) diff --git a/provider/kv/kv.go b/provider/kv/kv.go index aaeced042..b6393cc24 100644 --- a/provider/kv/kv.go +++ b/provider/kv/kv.go @@ -4,10 +4,8 @@ import ( "errors" "fmt" "strings" - "text/template" "time" - "github.com/BurntSushi/ty/fun" "github.com/cenk/backoff" "github.com/containous/traefik/job" "github.com/containous/traefik/log" @@ -27,7 +25,7 @@ type Provider struct { Username string `description:"KV Username"` Password string `description:"KV Password"` storeType store.Backend - kvclient store.Store + kvClient store.Store } // CreateStore create the K/V store @@ -58,16 +56,16 @@ func (p *Provider) SetStoreType(storeType store.Backend) { p.storeType = storeType } -// SetKVClient kvclient setter +// SetKVClient kvClient setter func (p *Provider) SetKVClient(kvClient store.Store) { - p.kvclient = kvClient + p.kvClient = kvClient } func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix string, stop chan bool) error { operation := func() error { - events, err := p.kvclient.WatchTree(p.Prefix, make(chan struct{}), nil) + events, err := p.kvClient.WatchTree(p.Prefix, make(chan struct{}), nil) if err != nil { - return fmt.Errorf("Failed to KV WatchTree: %v", err) + return fmt.Errorf("failed to KV WatchTree: %v", err) } for { select { @@ -77,7 +75,7 @@ func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix if !ok { return errors.New("watchtree channel closed") } - configuration := p.loadConfig() + configuration := p.buildConfiguration() if configuration != nil { configurationChan <- types.ConfigMessage{ ProviderName: string(p.storeType), @@ -93,7 +91,7 @@ func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix } err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify) if err != nil { - return fmt.Errorf("Cannot connect to KV server: %v", err) + return fmt.Errorf("cannot connect to KV server: %v", err) } return nil } @@ -102,8 +100,8 @@ func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { p.Constraints = append(p.Constraints, constraints...) operation := func() error { - if _, err := p.kvclient.Exists(p.Prefix+"/qmslkjdfmqlskdjfmqlksjazçueznbvbwzlkajzebvkwjdcqmlsfj", nil); err != nil { - return fmt.Errorf("Failed to test KV store connection: %v", err) + if _, err := p.kvClient.Exists(p.Prefix+"/qmslkjdfmqlskdjfmqlksjazçueznbvbwzlkajzebvkwjdcqmlsfj", nil); err != nil { + return fmt.Errorf("failed to test KV store connection: %v", err) } if p.Watch { pool.Go(func(stop chan bool) { @@ -113,7 +111,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s } }) } - configuration := p.loadConfig() + configuration := p.buildConfiguration() configurationChan <- types.ConfigMessage{ ProviderName: string(p.storeType), Configuration: configuration, @@ -125,142 +123,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s } err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify) if err != nil { - return fmt.Errorf("Cannot connect to KV server: %v", err) + return fmt.Errorf("cannot connect to KV server: %v", err) } return nil } - -func (p *Provider) loadConfig() *types.Configuration { - templateObjects := struct { - Prefix string - }{ - // Allow `/traefik/alias` to supersede `p.Prefix` - strings.TrimSuffix(p.get(p.Prefix, p.Prefix+"/alias"), "/"), - } - - var KvFuncMap = template.FuncMap{ - "List": p.list, - "ListServers": p.listServers, - "Get": p.get, - "SplitGet": p.splitGet, - "Last": p.last, - "getSticky": p.getSticky, - "hasStickinessLabel": p.hasStickinessLabel, - "getStickinessCookieName": p.getStickinessCookieName, - } - - configuration, err := p.GetConfiguration("templates/kv.tmpl", KvFuncMap, templateObjects) - if err != nil { - log.Error(err) - } - - for key, frontend := range configuration.Frontends { - if _, ok := configuration.Backends[frontend.Backend]; !ok { - delete(configuration.Frontends, key) - } - } - - return configuration -} - -func (p *Provider) list(keys ...string) []string { - joinedKeys := strings.Join(keys, "") - keysPairs, err := p.kvclient.List(joinedKeys, nil) - if err != nil { - log.Debugf("Cannot get keys %s %s ", joinedKeys, err) - return nil - } - directoryKeys := make(map[string]string) - for _, key := range keysPairs { - directory := strings.Split(strings.TrimPrefix(key.Key, joinedKeys), "/")[0] - directoryKeys[directory] = joinedKeys + directory - } - return fun.Values(directoryKeys).([]string) -} - -func (p *Provider) listServers(backend string) []string { - serverNames := p.list(backend, "/servers/") - return fun.Filter(func(serverName string) bool { - key := fmt.Sprint(serverName, "/url") - if _, err := p.kvclient.Get(key, nil); err != nil { - if err != store.ErrKeyNotFound { - log.Errorf("Failed to retrieve value for key %s: %s", key, err) - } - return false - } - return p.checkConstraints(serverName, "/tags") - }, serverNames).([]string) -} - -func (p *Provider) get(defaultValue string, keys ...string) string { - joinedKeys := strings.Join(keys, "") - if p.storeType == store.ETCD { - joinedKeys = strings.TrimPrefix(joinedKeys, "/") - } - keyPair, err := p.kvclient.Get(joinedKeys, nil) - if err != nil { - log.Debugf("Cannot get key %s %s, setting default %s", joinedKeys, err, defaultValue) - return defaultValue - } else if keyPair == nil { - log.Debugf("Cannot get key %s, setting default %s", joinedKeys, defaultValue) - return defaultValue - } - return string(keyPair.Value) -} - -func (p *Provider) splitGet(keys ...string) []string { - joinedKeys := strings.Join(keys, "") - keyPair, err := p.kvclient.Get(joinedKeys, nil) - if err != nil { - log.Debugf("Cannot get key %s %s, setting default empty", joinedKeys, err) - return []string{} - } else if keyPair == nil { - log.Debugf("Cannot get key %s, setting default %empty", joinedKeys) - return []string{} - } - return strings.Split(string(keyPair.Value), ",") -} - -func (p *Provider) last(key string) string { - splittedKey := strings.Split(key, "/") - return splittedKey[len(splittedKey)-1] -} - -func (p *Provider) checkConstraints(keys ...string) bool { - joinedKeys := strings.Join(keys, "") - keyPair, err := p.kvclient.Get(joinedKeys, nil) - - value := "" - if err == nil && keyPair != nil && keyPair.Value != nil { - value = string(keyPair.Value) - } - - constraintTags := strings.Split(value, ",") - ok, failingConstraint := p.MatchConstraints(constraintTags) - if !ok { - if failingConstraint != nil { - log.Debugf("Constraint %v not matching with following tags: %v", failingConstraint.String(), value) - } - return false - } - return true -} - -func (p *Provider) getSticky(rootPath string) string { - stickyValue := p.get("", rootPath, "/loadbalancer", "/sticky") - if len(stickyValue) > 0 { - log.Warnf("Deprecated configuration found: %s. Please use %s.", "loadbalancer/sticky", "loadbalancer/stickiness") - } else { - stickyValue = "false" - } - return stickyValue -} - -func (p *Provider) hasStickinessLabel(rootPath string) bool { - stickinessValue := p.get("false", rootPath, "/loadbalancer", "/stickiness") - return len(stickinessValue) > 0 && strings.EqualFold(strings.TrimSpace(stickinessValue), "true") -} - -func (p *Provider) getStickinessCookieName(rootPath string) string { - return p.get("", rootPath, "/loadbalancer", "/stickiness", "/cookiename") -} diff --git a/provider/kv/kv_config.go b/provider/kv/kv_config.go new file mode 100644 index 000000000..454808419 --- /dev/null +++ b/provider/kv/kv_config.go @@ -0,0 +1,178 @@ +package kv + +import ( + "fmt" + "sort" + "strconv" + "strings" + "text/template" + + "github.com/BurntSushi/ty/fun" + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/types" + "github.com/docker/libkv/store" +) + +func (p *Provider) buildConfiguration() *types.Configuration { + templateObjects := struct { + Prefix string + }{ + // Allow `/traefik/alias` to supersede `p.Prefix` + Prefix: strings.TrimSuffix(p.get(p.Prefix, p.Prefix+pathAlias), pathSeparator), + } + + var KvFuncMap = template.FuncMap{ + "List": p.list, + "ListServers": p.listServers, + "Get": p.get, + "SplitGet": p.splitGet, + "Last": p.last, + + // Backend functions + "getSticky": p.getSticky, + "hasStickinessLabel": p.hasStickinessLabel, + "getStickinessCookieName": p.getStickinessCookieName, + } + + configuration, err := p.GetConfiguration("templates/kv.tmpl", KvFuncMap, templateObjects) + if err != nil { + log.Error(err) + } + + for key, frontend := range configuration.Frontends { + if _, ok := configuration.Backends[frontend.Backend]; !ok { + delete(configuration.Frontends, key) + } + } + + return configuration +} + +func (p *Provider) getSticky(rootPath string) bool { + stickyValue := p.get("", rootPath, pathBackendLoadBalancerSticky) + if len(stickyValue) > 0 { + log.Warnf("Deprecated configuration found: %s. Please use %s.", pathBackendLoadBalancerSticky, pathBackendLoadBalancerStickiness) + } else { + return false + } + + sticky, err := strconv.ParseBool(stickyValue) + if err != nil { + log.Warnf("Invalid %s value: %s.", pathBackendLoadBalancerSticky, stickyValue) + } + + return sticky +} + +func (p *Provider) hasStickinessLabel(rootPath string) bool { + return p.getBool(false, rootPath, pathBackendLoadBalancerStickiness) +} + +func (p *Provider) getStickinessCookieName(rootPath string) string { + return p.get("", rootPath, pathBackendLoadBalancerStickinessCookieName) +} + +func (p *Provider) listServers(backend string) []string { + serverNames := p.list(backend, pathBackendServers) + return fun.Filter(p.serverFilter, serverNames).([]string) +} + +func (p *Provider) serverFilter(serverName string) bool { + key := fmt.Sprint(serverName, pathBackendServerURL) + if _, err := p.kvClient.Get(key, nil); err != nil { + if err != store.ErrKeyNotFound { + log.Errorf("Failed to retrieve value for key %s: %s", key, err) + } + return false + } + return p.checkConstraints(serverName, pathTags) +} + +func (p *Provider) checkConstraints(keys ...string) bool { + joinedKeys := strings.Join(keys, "") + keyPair, err := p.kvClient.Get(joinedKeys, nil) + + value := "" + if err == nil && keyPair != nil && keyPair.Value != nil { + value = string(keyPair.Value) + } + + constraintTags := label.SplitAndTrimString(value, ",") + ok, failingConstraint := p.MatchConstraints(constraintTags) + if !ok { + if failingConstraint != nil { + log.Debugf("Constraint %v not matching with following tags: %v", failingConstraint.String(), value) + } + return false + } + return true +} + +func (p *Provider) get(defaultValue string, keyParts ...string) string { + key := strings.Join(keyParts, "") + + if p.storeType == store.ETCD { + key = strings.TrimPrefix(key, pathSeparator) + } + + keyPair, err := p.kvClient.Get(key, nil) + if err != nil { + log.Debugf("Cannot get key %s %s, setting default %s", key, err, defaultValue) + return defaultValue + } else if keyPair == nil { + log.Debugf("Cannot get key %s, setting default %s", key, defaultValue) + return defaultValue + } + + return string(keyPair.Value) +} + +func (p *Provider) getBool(defaultValue bool, keyParts ...string) bool { + rawValue := p.get(strconv.FormatBool(defaultValue), keyParts...) + + if len(rawValue) == 0 { + return defaultValue + } + + value, err := strconv.ParseBool(rawValue) + if err != nil { + log.Errorf("Invalid value for %v: %s", keyParts, rawValue) + return defaultValue + } + return value +} + +func (p *Provider) list(keyParts ...string) []string { + rootKey := strings.Join(keyParts, "") + + keysPairs, err := p.kvClient.List(rootKey, nil) + if err != nil { + log.Debugf("Cannot list keys under %q: %v", rootKey, err) + return nil + } + + directoryKeys := make(map[string]string) + for _, key := range keysPairs { + directory := strings.Split(strings.TrimPrefix(key.Key, rootKey), pathSeparator)[0] + directoryKeys[directory] = rootKey + directory + } + + keys := fun.Values(directoryKeys).([]string) + sort.Strings(keys) + return keys +} + +func (p *Provider) splitGet(keyParts ...string) []string { + value := p.get("", keyParts...) + + if len(value) == 0 { + return nil + } + return label.SplitAndTrimString(value, ",") +} + +func (p *Provider) last(key string) string { + index := strings.LastIndex(key, pathSeparator) + return key[index+1:] +} diff --git a/provider/kv/kv_config_test.go b/provider/kv/kv_config_test.go new file mode 100644 index 000000000..416b929ae --- /dev/null +++ b/provider/kv/kv_config_test.go @@ -0,0 +1,579 @@ +package kv + +import ( + "sort" + "strconv" + "testing" + + "github.com/containous/traefik/provider/label" + "github.com/containous/traefik/tls" + "github.com/containous/traefik/types" + "github.com/docker/libkv/store" + "github.com/stretchr/testify/assert" +) + +func aKVPair(key string, value string) *store.KVPair { + return &store.KVPair{Key: key, Value: []byte(value)} +} + +func TestProviderBuildConfiguration(t *testing.T) { + testCases := []struct { + desc string + kvPairs []*store.KVPair + expected *types.Configuration + }{ + { + desc: "name with dot", + kvPairs: filler("traefik", + frontend("frontend.with.dot", + withPair("backend", "backend.with.dot.too"), + withPair("routes/route.with.dot/rule", "Host:test.localhost")), + backend("backend.with.dot.too", + withPair("servers/server.with.dot/url", "http://172.17.0.2:80"), + withPair("servers/server.with.dot/weight", "0"), + withPair("servers/server.with.dot.without.url/weight", "0")), + ), + expected: &types.Configuration{ + Backends: map[string]*types.Backend{ + "backend.with.dot.too": { + Servers: map[string]types.Server{ + "server.with.dot": { + URL: "http://172.17.0.2:80", + Weight: 0, + }, + }, + }, + }, + Frontends: map[string]*types.Frontend{ + "frontend.with.dot": { + Backend: "backend.with.dot.too", + PassHostHeader: true, + EntryPoints: []string{}, + Routes: map[string]types.Route{ + "route.with.dot": { + Rule: "Host:test.localhost", + }, + }, + }, + }, + }, + }, + { + desc: "all parameters", + kvPairs: filler("traefik", + backend("backend1", + withPair("healthcheck/path", "/health"), + withPair("healthcheck/port", "80"), + withPair("healthcheck/interval", "30s"), + withPair("maxconn/amount", "5"), + withPair("maxconn/extractorfunc", "client.ip"), + withPair(pathBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), + withPair(pathBackendLoadBalancerMethod, "drr"), + withPair(pathBackendLoadBalancerSticky, "true"), + withPair(pathBackendLoadBalancerStickiness, "true"), + withPair(pathBackendLoadBalancerStickinessCookieName, "tomate"), + withPair("servers/server1/url", "http://172.17.0.2:80"), + withPair("servers/server1/weight", "0"), + withPair("servers/server2/weight", "0")), + frontend("frontend1", + withPair(pathFrontendBackend, "backend1"), + withPair(pathFrontendPriority, "6"), + withPair(pathFrontendPassHostHeader, "false"), + withPair(pathFrontendEntryPoints, "http,https"), + withPair("routes/route1/rule", "Host:test.localhost"), + withPair("routes/route2/rule", "Path:/foo")), + entry("tlsconfiguration/foo", + withPair("entrypoints", "http,https"), + withPair("certificate/certfile", "certfile1"), + withPair("certificate/keyfile", "keyfile1")), + entry("tlsconfiguration/bar", + withPair("entrypoints", "http,https"), + withPair("certificate/certfile", "certfile2"), + withPair("certificate/keyfile", "keyfile2")), + ), + expected: &types.Configuration{ + Backends: map[string]*types.Backend{ + "backend1": { + Servers: map[string]types.Server{ + "server1": { + URL: "http://172.17.0.2:80", + Weight: 0, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 1", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "tomate", + }, + }, + MaxConn: &types.MaxConn{ + Amount: 5, + ExtractorFunc: "client.ip", + }, + HealthCheck: &types.HealthCheck{ + Path: "/health", + Port: 0, + Interval: "30s", + }, + }, + }, + Frontends: map[string]*types.Frontend{ + "frontend1": { + Priority: 6, + EntryPoints: []string{"http", "https"}, + Backend: "backend1", + Routes: map[string]types.Route{ + "route1": { + Rule: "Host:test.localhost", + }, + "route2": { + Rule: "Path:/foo", + }, + }, + }, + }, + TLSConfiguration: []*tls.Configuration{ + { + EntryPoints: []string{"http", "https"}, + Certificate: &tls.Certificate{ + CertFile: "certfile2", + KeyFile: "keyfile2", + }, + }, + { + EntryPoints: []string{"http", "https"}, + Certificate: &tls.Certificate{ + CertFile: "certfile1", + KeyFile: "keyfile1", + }, + }, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + Prefix: "traefik", + kvClient: &Mock{ + KVPairs: test.kvPairs, + }, + } + + actual := p.buildConfiguration() + assert.NotNil(t, actual) + + assert.EqualValues(t, test.expected.Backends, actual.Backends) + assert.EqualValues(t, test.expected.Frontends, actual.Frontends) + assert.EqualValues(t, test.expected, actual) + }) + } +} + +func TestProviderList(t *testing.T) { + testCases := []struct { + desc string + kvPairs []*store.KVPair + kvError error + keyParts []string + expected []string + }{ + { + desc: "empty key parts and empty store", + keyParts: []string{}, + expected: []string{}, + }, + { + desc: "when non existing key and empty store", + keyParts: []string{"traefik"}, + expected: []string{}, + }, + { + desc: "when non existing key", + kvPairs: []*store.KVPair{ + aKVPair("foo", "bar"), + }, + keyParts: []string{"bar"}, + expected: []string{}, + }, + { + desc: "when one key", + kvPairs: []*store.KVPair{ + aKVPair("foo", "bar"), + }, + keyParts: []string{"foo"}, + expected: []string{"foo"}, + }, + { + desc: "when multiple sub keys and nested sub key", + kvPairs: []*store.KVPair{ + aKVPair("foo/baz/1", "bar"), + aKVPair("foo/baz/2", "bar"), + aKVPair("foo/baz/biz/1", "bar"), + }, + keyParts: []string{"foo", "/baz/"}, + expected: []string{"foo/baz/1", "foo/baz/2"}, + }, + { + desc: "when KV error", + kvError: store.ErrNotReachable, + kvPairs: []*store.KVPair{ + aKVPair("foo/baz/1", "bar1"), + aKVPair("foo/baz/2", "bar2"), + aKVPair("foo/baz/biz/1", "bar3"), + }, + keyParts: []string{"foo/baz/1"}, + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + kvClient: newKvClientMock(test.kvPairs, test.kvError), + } + + actual := p.list(test.keyParts...) + + sort.Strings(test.expected) + assert.Equal(t, test.expected, actual, "key: %v", test.keyParts) + }) + } +} + +func TestProviderGet(t *testing.T) { + testCases := []struct { + desc string + kvPairs []*store.KVPair + storeType store.Backend + keyParts []string + defaultValue string + kvError error + expected string + }{ + { + desc: "when empty key parts, empty store", + defaultValue: "circle", + keyParts: []string{}, + expected: "circle", + }, + { + desc: "when non existing key", + defaultValue: "circle", + kvPairs: []*store.KVPair{ + aKVPair("foo", "bar"), + }, + keyParts: []string{"bar"}, + expected: "circle", + }, + { + desc: "when one part key", + kvPairs: []*store.KVPair{ + aKVPair("foo", "bar"), + }, + keyParts: []string{"foo"}, + expected: "bar", + }, + { + desc: "when several parts key", + kvPairs: []*store.KVPair{ + aKVPair("foo/baz/1", "bar1"), + aKVPair("foo/baz/2", "bar2"), + aKVPair("foo/baz/biz/1", "bar3"), + }, + keyParts: []string{"foo", "/baz/", "2"}, + expected: "bar2", + }, + { + desc: "when several parts key, starts with /", + defaultValue: "circle", + kvPairs: []*store.KVPair{ + aKVPair("foo/baz/1", "bar1"), + aKVPair("foo/baz/2", "bar2"), + aKVPair("foo/baz/biz/1", "bar3"), + }, + keyParts: []string{"/foo", "/baz/", "2"}, + expected: "circle", + }, + { + desc: "when several parts key starts with /, ETCD v2", + storeType: store.ETCD, + kvPairs: []*store.KVPair{ + aKVPair("foo/baz/1", "bar1"), + aKVPair("foo/baz/2", "bar2"), + aKVPair("foo/baz/biz/1", "bar3"), + }, + keyParts: []string{"/foo", "/baz/", "2"}, + expected: "bar2", + }, + { + desc: "when KV error", + kvError: store.ErrNotReachable, + kvPairs: []*store.KVPair{ + aKVPair("foo/baz/1", "bar1"), + aKVPair("foo/baz/2", "bar2"), + aKVPair("foo/baz/biz/1", "bar3"), + }, + keyParts: []string{"foo/baz/1"}, + expected: "", + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + kvClient: newKvClientMock(test.kvPairs, test.kvError), + storeType: test.storeType, + } + + actual := p.get(test.defaultValue, test.keyParts...) + + assert.Equal(t, test.expected, actual, "key %v", test.keyParts) + }) + } +} + +func TestProviderLast(t *testing.T) { + p := &Provider{} + + testCases := []struct { + key string + expected string + }{ + { + key: "", + expected: "", + }, + { + key: "foo", + expected: "foo", + }, + { + key: "foo/bar", + expected: "bar", + }, + { + key: "foo/bar/baz", + expected: "baz", + }, + // FIXME is this wanted ? + { + key: "foo/bar/", + expected: "", + }, + } + + for i, test := range testCases { + test := test + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() + + actual := p.last(test.key) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestProviderSplitGet(t *testing.T) { + testCases := []struct { + desc string + kvPairs []*store.KVPair + kvError error + keyParts []string + expected []string + }{ + { + desc: "when has value", + kvPairs: filler("traefik", + frontend("foo", + withPair("bar", "courgette, carotte, tomate, aubergine"), + ), + ), + keyParts: []string{"traefik/frontends/foo/bar"}, + expected: []string{"courgette", "carotte", "tomate", "aubergine"}, + }, + { + desc: "when empty value", + kvPairs: filler("traefik", + frontend("foo", + withPair("bar", ""), + ), + ), + keyParts: []string{"traefik/frontends/foo/bar"}, + expected: nil, + }, + { + desc: "when not existing key", + kvPairs: nil, + keyParts: []string{"traefik/frontends/foo/bar"}, + expected: nil, + }, + { + desc: "when KV error", + kvError: store.ErrNotReachable, + kvPairs: filler("traefik", + frontend("foo", + withPair("bar", ""), + ), + ), + keyParts: []string{"traefik/frontends/foo/bar"}, + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + kvClient: newKvClientMock(test.kvPairs, test.kvError), + } + + values := p.splitGet(test.keyParts...) + + assert.Equal(t, test.expected, values, "key: %v", test.keyParts) + }) + } +} + +func TestProviderGetBool(t *testing.T) { + testCases := []struct { + desc string + kvPairs []*store.KVPair + kvError error + keyParts []string + expected bool + }{ + { + desc: "when value is 'true", + kvPairs: filler("traefik", + frontend("foo", + withPair("bar", "true"), + ), + ), + keyParts: []string{"traefik/frontends/foo/bar"}, + expected: true, + }, + { + desc: "when value is 'false", + kvPairs: filler("traefik", + frontend("foo", + withPair("bar", "false"), + ), + ), + keyParts: []string{"traefik/frontends/foo/bar"}, + expected: false, + }, + { + desc: "when empty value", + kvPairs: filler("traefik", + frontend("foo", + withPair("bar", ""), + ), + ), + keyParts: []string{"traefik/frontends/foo/bar"}, + expected: false, + }, + { + desc: "when not existing key", + kvPairs: nil, + keyParts: []string{"traefik/frontends/foo/bar"}, + expected: false, + }, + { + desc: "when KV error", + kvError: store.ErrNotReachable, + kvPairs: filler("traefik", + frontend("foo", + withPair("bar", "true"), + ), + ), + keyParts: []string{"traefik/frontends/foo/bar"}, + expected: false, + }, + } + + for i, test := range testCases { + test := test + t.Run(strconv.Itoa(i), func(t *testing.T) { + t.Parallel() + + p := &Provider{ + kvClient: newKvClientMock(test.kvPairs, test.kvError), + } + + actual := p.getBool(false, test.keyParts...) + + assert.Equal(t, test.expected, actual, "key: %v", test.keyParts) + }) + } +} + +func TestProviderHasStickinessLabel(t *testing.T) { + testCases := []struct { + desc string + kvPairs []*store.KVPair + rootPath string + expected bool + }{ + { + desc: "without option", + expected: false, + }, + { + desc: "with cookie name without stickiness=true", + rootPath: "traefik/backends/foo", + kvPairs: filler("traefik", + backend("foo", + withPair(pathBackendLoadBalancerStickinessCookieName, "aubergine"), + ), + ), + expected: false, + }, + { + desc: "stickiness=true", + rootPath: "traefik/backends/foo", + kvPairs: filler("traefik", + backend("foo", + withPair(pathBackendLoadBalancerStickiness, "true"), + ), + ), + expected: true, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + p := &Provider{ + kvClient: &Mock{ + KVPairs: test.kvPairs, + }, + } + + actual := p.hasStickinessLabel(test.rootPath) + + if actual != test.expected { + t.Fatalf("expected %v, got %v", test.expected, actual) + } + }) + } +} diff --git a/provider/kv/kv_mock_test.go b/provider/kv/kv_mock_test.go index 12d001deb..878d385da 100644 --- a/provider/kv/kv_mock_test.go +++ b/provider/kv/kv_mock_test.go @@ -4,18 +4,9 @@ import ( "errors" "strings" - "github.com/containous/traefik/types" "github.com/docker/libkv/store" ) -type KvMock struct { - Provider -} - -func (provider *KvMock) loadConfig() *types.Configuration { - return nil -} - // Override Get/List to return a error type KvError struct { Get error @@ -29,6 +20,20 @@ type Mock struct { WatchTreeMethod func() <-chan []*store.KVPair } +func newKvClientMock(kvPairs []*store.KVPair, err error) *Mock { + mock := &Mock{ + KVPairs: kvPairs, + } + + if err != nil { + mock.Error = KvError{ + Get: err, + List: err, + } + } + return mock +} + func (s *Mock) Put(key string, value []byte, opts *store.WriteOptions) error { return errors.New("Put not supported") } @@ -82,9 +87,9 @@ func (s *Mock) List(prefix string, options *store.ReadOptions) ([]*store.KVPair, if err := s.Error.List; err != nil { return nil, err } - kv := []*store.KVPair{} + var kv []*store.KVPair for _, kvPair := range s.KVPairs { - if strings.HasPrefix(kvPair.Key, prefix) && !strings.ContainsAny(strings.TrimPrefix(kvPair.Key, prefix), "/") { + if strings.HasPrefix(kvPair.Key, prefix) && !strings.ContainsAny(strings.TrimPrefix(kvPair.Key, prefix), pathSeparator) { kv = append(kv, kvPair) } } diff --git a/provider/kv/kv_test.go b/provider/kv/kv_test.go index ea1889f36..3a7ac472b 100644 --- a/provider/kv/kv_test.go +++ b/provider/kv/kv_test.go @@ -1,8 +1,6 @@ package kv import ( - "reflect" - "sort" "testing" "time" @@ -10,241 +8,14 @@ import ( "github.com/docker/libkv/store" ) -func TestKvList(t *testing.T) { - cases := []struct { - provider *Provider - keys []string - expected []string - }{ - { - provider: &Provider{ - kvclient: &Mock{}, - }, - keys: []string{}, - expected: []string{}, - }, - { - provider: &Provider{ - kvclient: &Mock{}, - }, - keys: []string{"traefik"}, - expected: []string{}, - }, - { - provider: &Provider{ - kvclient: &Mock{ - KVPairs: []*store.KVPair{ - { - Key: "foo", - Value: []byte("bar"), - }, - }, - }, - }, - keys: []string{"bar"}, - expected: []string{}, - }, - { - provider: &Provider{ - kvclient: &Mock{ - KVPairs: []*store.KVPair{ - { - Key: "foo", - Value: []byte("bar"), - }, - }, - }, - }, - keys: []string{"foo"}, - expected: []string{"foo"}, - }, - { - provider: &Provider{ - kvclient: &Mock{ - KVPairs: []*store.KVPair{ - { - Key: "foo/baz/1", - Value: []byte("bar"), - }, - { - Key: "foo/baz/2", - Value: []byte("bar"), - }, - { - Key: "foo/baz/biz/1", - Value: []byte("bar"), - }, - }, - }, - }, - keys: []string{"foo", "/baz/"}, - expected: []string{"foo/baz/1", "foo/baz/2"}, - }, - } - - for _, c := range cases { - actual := c.provider.list(c.keys...) - sort.Strings(actual) - sort.Strings(c.expected) - if !reflect.DeepEqual(actual, c.expected) { - t.Fatalf("expected %v, got %v for %v and %v", c.expected, actual, c.keys, c.provider) - } - } - - // Error case - provider := &Provider{ - kvclient: &Mock{ - Error: KvError{ - List: store.ErrKeyNotFound, - }, - }, - } - actual := provider.list("anything") - if actual != nil { - t.Fatalf("Should have return nil, got %v", actual) - } -} - -func TestKvGet(t *testing.T) { - cases := []struct { - provider *Provider - keys []string - expected string - }{ - { - provider: &Provider{ - kvclient: &Mock{}, - }, - keys: []string{}, - expected: "", - }, - { - provider: &Provider{ - kvclient: &Mock{}, - }, - keys: []string{"traefik"}, - expected: "", - }, - { - provider: &Provider{ - kvclient: &Mock{ - KVPairs: []*store.KVPair{ - { - Key: "foo", - Value: []byte("bar"), - }, - }, - }, - }, - keys: []string{"bar"}, - expected: "", - }, - { - provider: &Provider{ - kvclient: &Mock{ - KVPairs: []*store.KVPair{ - { - Key: "foo", - Value: []byte("bar"), - }, - }, - }, - }, - keys: []string{"foo"}, - expected: "bar", - }, - { - provider: &Provider{ - kvclient: &Mock{ - KVPairs: []*store.KVPair{ - { - Key: "foo/baz/1", - Value: []byte("bar1"), - }, - { - Key: "foo/baz/2", - Value: []byte("bar2"), - }, - { - Key: "foo/baz/biz/1", - Value: []byte("bar3"), - }, - }, - }, - }, - keys: []string{"foo", "/baz/", "2"}, - expected: "bar2", - }, - } - - for _, c := range cases { - actual := c.provider.get("", c.keys...) - if actual != c.expected { - t.Fatalf("expected %v, got %v for %v and %v", c.expected, actual, c.keys, c.provider) - } - } - - // Error case - provider := &Provider{ - kvclient: &Mock{ - Error: KvError{ - Get: store.ErrKeyNotFound, - }, - }, - } - actual := provider.get("", "anything") - if actual != "" { - t.Fatalf("Should have return nil, got %v", actual) - } -} - -func TestKvLast(t *testing.T) { - cases := []struct { - key string - expected string - }{ - { - key: "", - expected: "", - }, - { - key: "foo", - expected: "foo", - }, - { - key: "foo/bar", - expected: "bar", - }, - { - key: "foo/bar/baz", - expected: "baz", - }, - // FIXME is this wanted ? - { - key: "foo/bar/", - expected: "", - }, - } - - provider := &Provider{} - for _, c := range cases { - actual := provider.last(c.key) - if actual != c.expected { - t.Fatalf("expected %s, got %s", c.expected, actual) - } - } -} - func TestKvWatchTree(t *testing.T) { returnedChans := make(chan chan []*store.KVPair) - provider := &KvMock{ - Provider{ - kvclient: &Mock{ - WatchTreeMethod: func() <-chan []*store.KVPair { - c := make(chan []*store.KVPair, 10) - returnedChans <- c - return c - }, + provider := Provider{ + kvClient: &Mock{ + WatchTreeMethod: func() <-chan []*store.KVPair { + c := make(chan []*store.KVPair, 10) + returnedChans <- c + return c }, }, } @@ -277,146 +48,3 @@ func TestKvWatchTree(t *testing.T) { default: } } - -func TestKVLoadConfig(t *testing.T) { - provider := &Provider{ - Prefix: "traefik", - kvclient: &Mock{ - KVPairs: []*store.KVPair{ - { - Key: "traefik/frontends/frontend.with.dot", - Value: []byte(""), - }, - { - Key: "traefik/frontends/frontend.with.dot/backend", - Value: []byte("backend.with.dot.too"), - }, - { - Key: "traefik/frontends/frontend.with.dot/routes", - Value: []byte(""), - }, - { - Key: "traefik/frontends/frontend.with.dot/routes/route.with.dot", - Value: []byte(""), - }, - { - Key: "traefik/frontends/frontend.with.dot/routes/route.with.dot/rule", - Value: []byte("Host:test.localhost"), - }, - { - Key: "traefik/backends/backend.with.dot.too", - Value: []byte(""), - }, - { - Key: "traefik/backends/backend.with.dot.too/servers", - Value: []byte(""), - }, - { - Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot", - Value: []byte(""), - }, - { - Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot/url", - Value: []byte("http://172.17.0.2:80"), - }, - { - Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot/weight", - Value: []byte("0"), - }, - { - Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot.without.url", - Value: []byte(""), - }, - { - Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot.without.url/weight", - Value: []byte("0"), - }, - }, - }, - } - actual := provider.loadConfig() - expected := &types.Configuration{ - Backends: map[string]*types.Backend{ - "backend.with.dot.too": { - Servers: map[string]types.Server{ - "server.with.dot": { - URL: "http://172.17.0.2:80", - Weight: 0, - }, - }, - CircuitBreaker: nil, - LoadBalancer: nil, - }, - }, - Frontends: map[string]*types.Frontend{ - "frontend.with.dot": { - Backend: "backend.with.dot.too", - PassHostHeader: true, - EntryPoints: []string{}, - Routes: map[string]types.Route{ - "route.with.dot": { - Rule: "Host:test.localhost", - }, - }, - }, - }, - } - if !reflect.DeepEqual(actual.Backends, expected.Backends) { - t.Fatalf("expected %+v, got %+v", expected.Backends, actual.Backends) - } - if !reflect.DeepEqual(actual.Frontends, expected.Frontends) { - t.Fatalf("expected %+v, got %+v", expected.Frontends, actual.Frontends) - } -} - -func TestKVHasStickinessLabel(t *testing.T) { - testCases := []struct { - desc string - KVPairs []*store.KVPair - expected bool - }{ - { - desc: "without option", - expected: false, - }, - { - desc: "with cookie name without stickiness=true", - KVPairs: []*store.KVPair{ - { - Key: "/loadbalancer/stickiness/cookiename", - Value: []byte("foo"), - }, - }, - expected: false, - }, - { - desc: "stickiness=true", - KVPairs: []*store.KVPair{ - { - Key: "/loadbalancer/stickiness", - Value: []byte("true"), - }, - }, - expected: true, - }, - } - - for _, test := range testCases { - test := test - t.Run(test.desc, func(t *testing.T) { - t.Parallel() - - p := &Provider{ - kvclient: &Mock{ - KVPairs: test.KVPairs, - }, - } - - actual := p.hasStickinessLabel("") - - if actual != test.expected { - t.Fatalf("expected %v, got %v", test.expected, actual) - } - }) - } -}