1
0
Fork 0

Extract providers to their own package

This is just doing that and making it compile :)

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
Vincent Demeester 2017-04-17 12:50:02 +02:00
parent 2d00758b2e
commit 542c3673e4
36 changed files with 861 additions and 833 deletions

227
provider/kv/kv.go Normal file
View file

@ -0,0 +1,227 @@
package kv
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"
"github.com/containous/traefik/provider"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
"github.com/docker/libkv"
"github.com/docker/libkv/store"
)
// Provider holds common configurations of key-value providers.
type Provider struct {
provider.BaseProvider `mapstructure:",squash"`
Endpoint string `description:"Comma sepparated server endpoints"`
Prefix string `description:"Prefix used for KV store"`
TLS *provider.ClientTLS `description:"Enable TLS support"`
StoreType store.Backend
Kvclient store.Store
}
// CreateStore create the K/V store
func (p *Provider) CreateStore() (store.Store, error) {
storeConfig := &store.Config{
ConnectionTimeout: 30 * time.Second,
Bucket: "traefik",
}
if p.TLS != nil {
var err error
storeConfig.TLS, err = p.TLS.CreateTLSConfig()
if err != nil {
return nil, err
}
}
return libkv.NewStore(
p.StoreType,
strings.Split(p.Endpoint, ","),
storeConfig,
)
}
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{}))
if err != nil {
return fmt.Errorf("Failed to KV WatchTree: %v", err)
}
for {
select {
case <-stop:
return nil
case _, ok := <-events:
if !ok {
return errors.New("watchtree channel closed")
}
configuration := p.loadConfig()
if configuration != nil {
configurationChan <- types.ConfigMessage{
ProviderName: string(p.StoreType),
Configuration: configuration,
}
}
}
}
}
notify := func(err error, time time.Duration) {
log.Errorf("KV connection error: %+v, retrying in %s", err, time)
}
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 nil
}
// Provide provides the configuration to traefik via the configuration channel
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("qmslkjdfmqlskdjfmqlksjazçueznbvbwzlkajzebvkwjdcqmlsfj"); err != nil {
return fmt.Errorf("Failed to test KV store connection: %v", err)
}
if p.Watch {
pool.Go(func(stop chan bool) {
err := p.watchKv(configurationChan, p.Prefix, stop)
if err != nil {
log.Errorf("Cannot watch KV store: %v", err)
}
})
}
configuration := p.loadConfig()
configurationChan <- types.ConfigMessage{
ProviderName: string(p.StoreType),
Configuration: configuration,
}
return nil
}
notify := func(err error, time time.Duration) {
log.Errorf("KV connection error: %+v, retrying in %s", err, time)
}
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 nil
}
func (p *Provider) loadConfig() *types.Configuration {
templateObjects := struct {
Prefix string
}{
// Allow `/traefik/alias` to superesede `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,
}
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 == false {
delete(configuration.Frontends, key)
}
}
return configuration
}
func (p *Provider) list(keys ...string) []string {
joinedKeys := strings.Join(keys, "")
keysPairs, err := p.Kvclient.List(joinedKeys)
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); 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, "")
keyPair, err := p.Kvclient.Get(strings.TrimPrefix(joinedKeys, "/"))
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)
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)
value := ""
if err == nil && keyPair != nil && keyPair.Value != nil {
value = string(keyPair.Value)
}
constraintTags := strings.Split(value, ",")
ok, failingConstraint := p.MatchConstraints(constraintTags)
if ok == false {
if failingConstraint != nil {
log.Debugf("Constraint %v not matching with following tags: %v", failingConstraint.String(), value)
}
return false
}
return true
}

467
provider/kv/kv_test.go Normal file
View file

@ -0,0 +1,467 @@
package kv
import (
"errors"
"reflect"
"sort"
"strings"
"testing"
"time"
"github.com/containous/traefik/types"
"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)
}
}
}
type KvMock struct {
Provider
}
func (provider *KvMock) loadConfig() *types.Configuration {
return nil
}
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
},
},
},
}
configChan := make(chan types.ConfigMessage)
go func() {
provider.watchKv(configChan, "prefix", make(chan bool, 1))
}()
select {
case c1 := <-returnedChans:
c1 <- []*store.KVPair{}
<-configChan
close(c1) // WatchTree chans can close due to error
case <-time.After(1 * time.Second):
t.Fatalf("Failed to create a new WatchTree chan")
}
select {
case c2 := <-returnedChans:
c2 <- []*store.KVPair{}
<-configChan
case <-time.After(1 * time.Second):
t.Fatalf("Failed to create a new WatchTree chan")
}
select {
case _ = <-configChan:
t.Fatalf("configChan should be empty")
default:
}
}
// Override Get/List to return a error
type KvError struct {
Get error
List error
}
// Extremely limited mock store so we can test initialization
type Mock struct {
Error KvError
KVPairs []*store.KVPair
WatchTreeMethod func() <-chan []*store.KVPair
}
func (s *Mock) Put(key string, value []byte, opts *store.WriteOptions) error {
return errors.New("Put not supported")
}
func (s *Mock) Get(key string) (*store.KVPair, error) {
if err := s.Error.Get; err != nil {
return nil, err
}
for _, kvPair := range s.KVPairs {
if kvPair.Key == key {
return kvPair, nil
}
}
return nil, store.ErrKeyNotFound
}
func (s *Mock) Delete(key string) error {
return errors.New("Delete not supported")
}
// Exists mock
func (s *Mock) Exists(key string) (bool, error) {
return false, errors.New("Exists not supported")
}
// Watch mock
func (s *Mock) Watch(key string, stopCh <-chan struct{}) (<-chan *store.KVPair, error) {
return nil, errors.New("Watch not supported")
}
// WatchTree mock
func (s *Mock) WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*store.KVPair, error) {
return s.WatchTreeMethod(), nil
}
// NewLock mock
func (s *Mock) NewLock(key string, options *store.LockOptions) (store.Locker, error) {
return nil, errors.New("NewLock not supported")
}
// List mock
func (s *Mock) List(prefix string) ([]*store.KVPair, error) {
if err := s.Error.List; err != nil {
return nil, err
}
kv := []*store.KVPair{}
for _, kvPair := range s.KVPairs {
if strings.HasPrefix(kvPair.Key, prefix) && !strings.ContainsAny(strings.TrimPrefix(kvPair.Key, prefix), "/") {
kv = append(kv, kvPair)
}
}
return kv, nil
}
// DeleteTree mock
func (s *Mock) DeleteTree(prefix string) error {
return errors.New("DeleteTree not supported")
}
// AtomicPut mock
func (s *Mock) AtomicPut(key string, value []byte, previous *store.KVPair, opts *store.WriteOptions) (bool, *store.KVPair, error) {
return false, nil, errors.New("AtomicPut not supported")
}
// AtomicDelete mock
func (s *Mock) AtomicDelete(key string, previous *store.KVPair) (bool, error) {
return false, errors.New("AtomicDelete not supported")
}
// Close mock
func (s *Mock) Close() {
return
}
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)
}
}