From 06d2f343dde940b1b3f29fca568cc76870503757 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Tue, 19 Apr 2016 22:06:33 +0200 Subject: [PATCH 01/15] Fix KV backend Signed-off-by: Emile Vauge --- examples/consul-config.sh | 8 ++-- provider/kv.go | 87 +++++++++++++++++++++++++-------------- 2 files changed, 59 insertions(+), 36 deletions(-) diff --git a/examples/consul-config.sh b/examples/consul-config.sh index 58952031e..e86c68201 100755 --- a/examples/consul-config.sh +++ b/examples/consul-config.sh @@ -17,11 +17,9 @@ curl -i -H "Accept: application/json" -X PUT -d "2" ht # frontend 1 curl -i -H "Accept: application/json" -X PUT -d "backend2" http://localhost:8500/v1/kv/traefik/frontends/frontend1/backend curl -i -H "Accept: application/json" -X PUT -d "http" http://localhost:8500/v1/kv/traefik/frontends/frontend1/entrypoints -curl -i -H "Accept: application/json" -X PUT -d "Host" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/rule -curl -i -H "Accept: application/json" -X PUT -d "test.localhost" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/value +curl -i -H "Accept: application/json" -X PUT -d "Host:test.localhost" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/rule # frontend 2 curl -i -H "Accept: application/json" -X PUT -d "backend1" http://localhost:8500/v1/kv/traefik/frontends/frontend2/backend -curl -i -H "Accept: application/json" -X PUT -d "http,https" http://localhost:8500/v1/kv/traefik/frontends/frontend2/entrypoints -curl -i -H "Accept: application/json" -X PUT -d "Path" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/rule -curl -i -H "Accept: application/json" -X PUT -d "/test" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/value +curl -i -H "Accept: application/json" -X PUT -d "http" http://localhost:8500/v1/kv/traefik/frontends/frontend2/entrypoints +curl -i -H "Accept: application/json" -X PUT -d "Path:/test" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/rule diff --git a/provider/kv.go b/provider/kv.go index aacddffd8..ebf55bce6 100644 --- a/provider/kv.go +++ b/provider/kv.go @@ -10,8 +10,10 @@ import ( "text/template" "time" + "errors" "github.com/BurntSushi/ty/fun" log "github.com/Sirupsen/logrus" + "github.com/cenkalti/backoff" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "github.com/docker/libkv" @@ -37,25 +39,38 @@ type KvTLS struct { } func (provider *Kv) watchKv(configurationChan chan<- types.ConfigMessage, prefix string, stop chan bool) { - for { + operation := func() error { events, err := provider.kvclient.WatchTree(provider.Prefix, make(chan struct{}) /* stop chan */) if err != nil { log.Errorf("Failed to WatchTree %s", err) - continue + return err } - select { - case <-stop: - return - case <-events: - configuration := provider.loadConfig() - if configuration != nil { - configurationChan <- types.ConfigMessage{ - ProviderName: string(provider.storeType), - Configuration: configuration, + for { + select { + case <-stop: + return nil + case _, ok := <-events: + if !ok { + return errors.New("watchtree channel closed") + } + configuration := provider.loadConfig() + if configuration != nil { + configurationChan <- types.ConfigMessage{ + ProviderName: string(provider.storeType), + Configuration: configuration, + } } } } } + + notify := func(err error, time time.Duration) { + log.Errorf("KV connection error %+v, retrying in %s", err, time) + } + err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify) + if err != nil { + log.Fatalf("Cannot connect to KV server %+v", err) + } } func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { @@ -90,27 +105,37 @@ func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage, pool * } } - kv, err := libkv.NewStore( - provider.storeType, - strings.Split(provider.Endpoint, ","), - storeConfig, - ) + operation := func() error { + kv, err := libkv.NewStore( + provider.storeType, + strings.Split(provider.Endpoint, ","), + storeConfig, + ) + if err != nil { + return err + } + if _, err := kv.List(""); err != nil { + return err + } + provider.kvclient = kv + if provider.Watch { + pool.Go(func(stop chan bool) { + provider.watchKv(configurationChan, provider.Prefix, stop) + }) + } + configuration := provider.loadConfig() + configurationChan <- types.ConfigMessage{ + ProviderName: string(provider.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(operation, backoff.NewExponentialBackOff(), notify) if err != nil { - return err - } - if _, err := kv.List(""); err != nil { - return err - } - provider.kvclient = kv - if provider.Watch { - pool.Go(func(stop chan bool) { - provider.watchKv(configurationChan, provider.Prefix, stop) - }) - } - configuration := provider.loadConfig() - configurationChan <- types.ConfigMessage{ - ProviderName: string(provider.storeType), - Configuration: configuration, + log.Fatalf("Cannot connect to KV server %+v", err) } return nil } From 4d22c45b760e613a34f42bd94e3ff5926f579b8f Mon Sep 17 00:00:00 2001 From: Pascal Borreli Date: Thu, 21 Apr 2016 23:38:44 +0100 Subject: [PATCH 02/15] Fixed typos --- README.md | 2 +- acme/acme.go | 2 +- docs/basics.md | 4 ++-- docs/toml.md | 2 +- examples/whoami.json | 2 +- rules.go | 2 +- types/types.go | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index efd0f31fd..f27915984 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ You can access to a simple HTML frontend of Træfik. ## Plumbing -- [Oxy](https://github.com/vulcand/oxy): an awsome proxy library made by Mailgun guys +- [Oxy](https://github.com/vulcand/oxy): an awesome proxy library made by Mailgun guys - [Gorilla mux](https://github.com/gorilla/mux): famous request router - [Negroni](https://github.com/codegangsta/negroni): web middlewares made simple - [Manners](https://github.com/mailgun/manners): graceful shutdown of http.Handler servers diff --git a/acme/acme.go b/acme/acme.go index 6ebbe6590..1a93d3e17 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -181,7 +181,7 @@ func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(doma acme.Logger = fmtlog.New(ioutil.Discard, "", 0) if len(a.StorageFile) == 0 { - return errors.New("Empty StorageFile, please provide a filenmae for certs storage") + return errors.New("Empty StorageFile, please provide a filename for certs storage") } log.Debugf("Generating default certificate...") diff --git a/docs/basics.md b/docs/basics.md index 749f77d1d..73d9089ac 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -19,7 +19,7 @@ Let's zoom on Træfɪk and have an overview of its internal architecture: ![Architecture](img/internal.png) - Incoming requests end on [entrypoints](#entrypoints), as the name suggests, they are the network entry points into Træfɪk (listening port, SSL, traffic redirection...). -- Traffic is then forwared to a matching [frontend](#frontends). A frontend defines routes from [entrypoints](#entrypoints) to [backends](#backends). +- Traffic is then forwarded to a matching [frontend](#frontends). A frontend defines routes from [entrypoints](#entrypoints) to [backends](#backends). Routes are created using requests fields (`Host`, `Path`, `Headers`...) and can match or not a request. - The [frontend](#frontends) will then send the request to a [backend](#backends). A backend can be composed by one or more [servers](#servers), and by a load-balancing strategy. - Finally, the [server](#servers) will forward the request to the corresponding microservice in the private network. @@ -142,7 +142,7 @@ For example: ## Servers -Servers are simply defined using a `URL`. You can also apply a custom `weight` to each server (this will be used by load-balacning). +Servers are simply defined using a `URL`. You can also apply a custom `weight` to each server (this will be used by load-balancing). Here is an example of backends and servers definition: diff --git a/docs/toml.md b/docs/toml.md index 7929f02bb..5d929ccd6 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -258,7 +258,7 @@ defaultEntryPoints = ["http", "https"] rule = "Path:/test" ``` -- or put your rules in a separate file, for example `rules.tml`: +- or put your rules in a separate file, for example `rules.toml`: ```toml # traefik.toml diff --git a/examples/whoami.json b/examples/whoami.json index 6b4cc719b..980316388 100644 --- a/examples/whoami.json +++ b/examples/whoami.json @@ -25,7 +25,7 @@ ], "labels": { "traefik.weight": "1", - "traefik.protocole": "http", + "traefik.protocol": "http", "traefik.frontend.rule" : "Host:test.marathon.localhost" } } diff --git a/rules.go b/rules.go index 23668725d..e6b299d86 100644 --- a/rules.go +++ b/rules.go @@ -116,7 +116,7 @@ func (r *Rules) Parse(expression string) (*mux.Route, error) { } parsedFunction, ok := functions[parsedFunctions[0]] if !ok { - return nil, errors.New("Error parsing rule: " + expression + ". Unknow function: " + parsedFunctions[0]) + return nil, errors.New("Error parsing rule: " + expression + ". Unknown function: " + parsedFunctions[0]) } parsedFunctions = append(parsedFunctions[:0], parsedFunctions[1:]...) fargs := func(c rune) bool { diff --git a/types/types.go b/types/types.go index 44661e836..eeedcb73b 100644 --- a/types/types.go +++ b/types/types.go @@ -13,7 +13,7 @@ type Backend struct { MaxConn *MaxConn `json:"maxConn,omitempty"` } -// MaxConn holds maximum connection configuraiton +// MaxConn holds maximum connection configuration type MaxConn struct { Amount int64 `json:"amount,omitempty"` ExtractorFunc string `json:"extractorFunc,omitempty"` From 6f13a2c0c7e253069695927e8169b0e563a71edd Mon Sep 17 00:00:00 2001 From: Poney baker Date: Fri, 22 Apr 2016 11:11:33 +0200 Subject: [PATCH 03/15] feat(consul-catalog): Remove frontend when backends disabled --- provider/consul_catalog.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/provider/consul_catalog.go b/provider/consul_catalog.go index ade8679c8..937f69aeb 100644 --- a/provider/consul_catalog.go +++ b/provider/consul_catalog.go @@ -146,9 +146,14 @@ func (provider *ConsulCatalog) buildConfig(catalog []catalogUpdate) *types.Confi allNodes := []*api.ServiceEntry{} services := []*serviceUpdate{} for _, info := range catalog { - if len(info.Nodes) > 0 { - services = append(services, info.Service) - allNodes = append(allNodes, info.Nodes...) + for _, node := range info.Nodes { + isEnabled := provider.getAttribute("enable", node.Service.Tags, "true") + if isEnabled != "false" && len(info.Nodes) > 0 { + services = append(services, info.Service) + allNodes = append(allNodes, info.Nodes...) + break + } + } } From 720912e8807cd1f8b09cdbe39cd846f5b2d88241 Mon Sep 17 00:00:00 2001 From: emile Date: Mon, 8 Feb 2016 21:57:32 +0100 Subject: [PATCH 04/15] Add kubernetes Ingress backend Signed-off-by: Emile Vauge --- cmd.go | 8 +- configuration.go | 10 +- examples/compose-k8s.yaml | 17 ++ examples/k8s.ingress.yaml | 93 ++++++++++ examples/k8s.namespace.sh | 10 ++ examples/k8s.rc.yaml | 36 ++++ glide.lock | 28 +-- glide.yaml | 358 +++++++++++++++++++------------------- integration/basic_test.go | 6 +- provider/docker_test.go | 12 +- provider/k8s/client.go | 163 +++++++++++++++++ provider/k8s/ingress.go | 151 ++++++++++++++++ provider/k8s/service.go | 313 +++++++++++++++++++++++++++++++++ provider/kubernetes.go | 164 +++++++++++++++++ provider/provider_test.go | 2 +- script/binary | 2 +- server.go | 3 + templates/kubernetes.tmpl | 16 ++ 18 files changed, 1191 insertions(+), 201 deletions(-) create mode 100644 examples/compose-k8s.yaml create mode 100644 examples/k8s.ingress.yaml create mode 100755 examples/k8s.namespace.sh create mode 100644 examples/k8s.rc.yaml create mode 100644 provider/k8s/client.go create mode 100644 provider/k8s/ingress.go create mode 100644 provider/k8s/service.go create mode 100644 provider/kubernetes.go create mode 100644 templates/kubernetes.tmpl diff --git a/cmd.go b/cmd.go index e4ea1a4b0..13796957b 100644 --- a/cmd.go +++ b/cmd.go @@ -51,6 +51,7 @@ var arguments = struct { etcd bool etcdTLS bool boltdb bool + kubernetes bool }{ GlobalConfiguration{ EntryPoints: make(EntryPoints), @@ -72,7 +73,8 @@ var arguments = struct { TLS: &provider.KvTLS{}, }, }, - Boltdb: &provider.BoltDb{}, + Boltdb: &provider.BoltDb{}, + Kubernetes: &provider.Kubernetes{}, }, false, false, @@ -86,6 +88,7 @@ var arguments = struct { false, false, false, + false, } func init() { @@ -167,6 +170,9 @@ func init() { traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Endpoint, "boltdb.endpoint", "127.0.0.1:4001", "Boltdb server endpoint") traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Prefix, "boltdb.prefix", "/traefik", "Prefix used for KV store") + traefikCmd.PersistentFlags().BoolVar(&arguments.kubernetes, "kubernetes", false, "Enable Kubernetes backend") + traefikCmd.PersistentFlags().StringVar(&arguments.Kubernetes.Endpoint, "kubernetes.endpoint", "127.0.0.1:8080", "Kubernetes server endpoint") + _ = viper.BindPFlag("configFile", traefikCmd.PersistentFlags().Lookup("configFile")) _ = viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut")) _ = viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel")) diff --git a/configuration.go b/configuration.go index a2cfd122c..693b74c90 100644 --- a/configuration.go +++ b/configuration.go @@ -37,6 +37,7 @@ type GlobalConfiguration struct { Etcd *provider.Etcd Zookeeper *provider.Zookepper Boltdb *provider.BoltDb + Kubernetes *provider.Kubernetes } // DefaultEntryPoints holds default entry points @@ -209,7 +210,11 @@ func LoadConfiguration() *GlobalConfiguration { viper.AddConfigPath("$HOME/.traefik/") // call multiple times to add many search paths viper.AddConfigPath(".") // optionally look for config in the working directory if err := viper.ReadInConfig(); err != nil { - fmtlog.Fatalf("Error reading file: %s", err) + if len(viper.ConfigFileUsed()) > 0 { + fmtlog.Printf("Error reading configuration file: %s", err) + } else { + fmtlog.Printf("No configuration file found") + } } if len(arguments.EntryPoints) > 0 { @@ -254,6 +259,9 @@ func LoadConfiguration() *GlobalConfiguration { if arguments.boltdb { viper.Set("boltdb", arguments.Boltdb) } + if arguments.kubernetes { + viper.Set("kubernetes", arguments.Kubernetes) + } if err := unmarshal(&configuration); err != nil { fmtlog.Fatalf("Error reading file: %s", err) diff --git a/examples/compose-k8s.yaml b/examples/compose-k8s.yaml new file mode 100644 index 000000000..e9abe96b4 --- /dev/null +++ b/examples/compose-k8s.yaml @@ -0,0 +1,17 @@ +# etcd: +# image: gcr.io/google_containers/etcd:2.2.1 +# net: host +# command: ['/usr/local/bin/etcd', '--addr=127.0.0.1:4001', '--bind-addr=0.0.0.0:4001', '--data-dir=/var/etcd/data'] + +kubelet: + image: gcr.io/google_containers/hyperkube-amd64:v1.2.2 + privileged: true + pid: host + net : host + volumes: + - /:/rootfs:ro + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:rw + - /var/lib/kubelet/:/var/lib/kubelet:rw + - /var/run:/var/run:rw + command: ['/hyperkube', 'kubelet', '--containerized', '--hostname-override=127.0.0.1', '--address=0.0.0.0', '--api-servers=http://localhost:8080', '--config=/etc/kubernetes/manifests', '--allow-privileged=true', '--v=2'] diff --git a/examples/k8s.ingress.yaml b/examples/k8s.ingress.yaml new file mode 100644 index 000000000..7963bb2a6 --- /dev/null +++ b/examples/k8s.ingress.yaml @@ -0,0 +1,93 @@ +# 3 Services for the 3 endpoints of the Ingress +apiVersion: v1 +kind: Service +metadata: + name: whoami-x + labels: + app: whoami +spec: + type: NodePort + ports: + - port: 80 + nodePort: 30301 + targetPort: 80 + protocol: TCP + name: http + selector: + app: whoami +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami-default + labels: + app: whoami +spec: + type: NodePort + ports: + - port: 80 + nodePort: 30302 + targetPort: 80 + protocol: TCP + name: http + selector: + app: whoami +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami-y + labels: + app: whoami +spec: + type: NodePort + ports: + - port: 80 + nodePort: 30284 + targetPort: 80 + protocol: TCP + name: http + selector: + app: whoami +--- +# A single RC matching all Services +apiVersion: v1 +kind: ReplicationController +metadata: + name: whoami +spec: + replicas: 1 + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: emilevauge/whoami + ports: + - containerPort: 80 +--- +# An Ingress with 2 hosts and 3 endpoints +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: whoamimap +spec: + rules: + - host: foo.localhost + http: + paths: + - path: /bar + backend: + serviceName: whoami-x + servicePort: 80 + - host: bar.localhost + http: + paths: + - backend: + serviceName: whoami-y + servicePort: 80 + - backend: + serviceName: whoami-x + servicePort: 80 diff --git a/examples/k8s.namespace.sh b/examples/k8s.namespace.sh new file mode 100755 index 000000000..235beee0a --- /dev/null +++ b/examples/k8s.namespace.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +kubectl create -f - << EOF +kind: Namespace +apiVersion: v1 +metadata: + name: kube-system + labels: + name: kube-system +EOF diff --git a/examples/k8s.rc.yaml b/examples/k8s.rc.yaml new file mode 100644 index 000000000..11963dfb2 --- /dev/null +++ b/examples/k8s.rc.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: ReplicationController +metadata: + name: traefik-ingress-controller + labels: + k8s-app: traefik-ingress-lb +spec: + replicas: 1 + selector: + k8s-app: traefik-ingress-lb + template: + metadata: + labels: + k8s-app: traefik-ingress-lb + name: traefik-ingress-lb + spec: + terminationGracePeriodSeconds: 60 + containers: + - image: containous/traefik:k8s + name: traefik-ingress-lb + imagePullPolicy: Always + # livenessProbe: + # httpGet: + # path: /healthz + # port: 10249 + # scheme: HTTP + # initialDelaySeconds: 30 + # timeoutSeconds: 5 + ports: + - containerPort: 80 + hostPort: 80 + - containerPort: 443 + hostPort: 443 + args: + - --kubernetes + - --logLevel=DEBUG \ No newline at end of file diff --git a/glide.lock b/glide.lock index 48df05ac2..a099df31a 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: fffa87220825895f7e3a6ceed3b13ecbf6bc934ab072fc9be3d00e3eef411ecb -updated: 2016-04-13T14:05:41.300658168+02:00 +hash: 6fe539ee86a9dc90a67b60f42b027c72359bed0ca22e7a94355ad80f37a32d68 +updated: 2016-04-18T21:31:13.195184921+02:00 imports: - name: github.com/alecthomas/template version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 @@ -8,7 +8,7 @@ imports: - name: github.com/boltdb/bolt version: 51f99c862475898df9773747d3accd05a7ca33c1 - name: github.com/BurntSushi/toml - version: bd2bdf7f18f849530ef7a1c29a4290217cab32a1 + version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f - name: github.com/BurntSushi/ty version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74 subpackages: @@ -29,6 +29,7 @@ imports: - memmetrics - roundrobin - utils + - connlimit - stream - name: github.com/coreos/go-etcd version: cc90c7b091275e606ad0ca7102a23fb2072f3f5e @@ -42,7 +43,6 @@ imports: version: ff6f38ccb69afa96214c7ee955359465d1fc767a subpackages: - reference - - digest - name: github.com/docker/docker version: f39987afe8d611407887b3094c03d6ba6a766a67 subpackages: @@ -94,13 +94,11 @@ imports: - client/transport - client/transport/cancellable - types/network - - types/reference - types/registry - types/time - - types/versions - types/blkiodev - name: github.com/docker/go-connections - version: f549a9393d05688dff0992ef3efd8bbe6c628aeb + version: 5b7154ba2efe13ff86ae8830a9e7cb120b080d6e subpackages: - nat - sockets @@ -148,7 +146,7 @@ imports: subpackages: - api - name: github.com/hashicorp/hcl - version: 2604f3bda7e8960c1be1063709e7d7f0765048d0 + version: 27a57f2605e04995c111273c263d51cee60d9bc4 subpackages: - hcl/ast - hcl/parser @@ -177,13 +175,17 @@ imports: - name: github.com/Microsoft/go-winio version: 862b6557927a5c5c81e411c12aa6de7e566cbb7a - name: github.com/miekg/dns - version: dd83d5cbcfd986f334b2747feeb907e281318fdf + version: a5cc44dc6b2eee8eddfd6581e1c6bb753ff0d176 - name: github.com/mitchellh/mapstructure version: d2dd0262208475919e1a362f675cfc0e7c10e905 +- name: github.com/moul/http2curl + version: 1812aee76a1ce98d604a44200c6a23c689b17a89 - name: github.com/opencontainers/runc version: 4ab132458fc3e9dbeea624153e0331952dc4c8d5 subpackages: - libcontainer/user +- name: github.com/parnurzeal/gorequest + version: 91b42fce877cc6af96c45818665a4c615cc5f4ee - name: github.com/pmezard/go-difflib version: d8ed2627bdf02c080bf22230dbb337003b7aba2d subpackages: @@ -203,7 +205,7 @@ imports: - name: github.com/spf13/jwalterweatherman version: 33c24e77fb80341fe7130ee7c594256ff08ccc46 - name: github.com/spf13/pflag - version: 1f296710f879815ad9e6d39d947c828c3e4b4c3d + version: 8f6a28b0916586e7f22fe931ae2fcfc380b1c0e6 - name: github.com/spf13/viper version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325 - name: github.com/streamrail/concurrent-map @@ -220,7 +222,7 @@ imports: - name: github.com/unrolled/render version: 26b4e3aac686940fe29521545afad9966ddfc80c - name: github.com/vdemeester/docker-events - version: 6ea3f28df37f29a47498bc8b32b36ad8491dbd37 + version: 1ecaca5890ef1ffd266fcbfdbe43073ef105704b - name: github.com/vdemeester/libkermit version: 7e4e689a6fa9281e0fb9b7b9c297e22d5342a5ec - name: github.com/vdemeester/shakers @@ -243,11 +245,11 @@ imports: - name: github.com/wendal/errors version: f66c77a7882b399795a8987ebf87ef64a427417e - name: github.com/xenolf/lego - version: 23e88185c255e95a106835d80e76e5a3a66d7c54 + version: 684400fe76a813e78d87803a62bc04d977c501d2 subpackages: - acme - name: golang.org/x/crypto - version: d68c3ecb62c850b645dc072a8d78006286bf81ca + version: 1777f3ba8c1fed80fcaec3317e3aaa4f627764d2 subpackages: - ocsp - name: golang.org/x/net diff --git a/glide.yaml b/glide.yaml index 1cd5b5719..87905ad71 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,177 +1,185 @@ package: main import: - - package: github.com/coreos/go-etcd - ref: cc90c7b091275e606ad0ca7102a23fb2072f3f5e - subpackages: - - etcd - - package: github.com/mailgun/log - ref: 44874009257d4d47ba9806f1b7f72a32a015e4d8 - - package: github.com/containous/oxy - ref: 021f82bd8260ba15f5862a9fe62018437720dff5 - subpackages: - - cbreaker - - forward - - memmetrics - - roundrobin - - utils - - package: github.com/hashicorp/consul - ref: de080672fee9e6104572eeea89eccdca135bb918 - subpackages: - - api - - package: github.com/samuel/go-zookeeper - ref: fa6674abf3f4580b946a01bf7a1ce4ba8766205b - subpackages: - - zk - - package: github.com/docker/libtrust - ref: 9cbd2a1374f46905c68a4eb3694a130610adc62a - - package: github.com/go-check/check - ref: 11d3bc7aa68e238947792f30573146a3231fc0f1 - - package: golang.org/x/net - ref: d9558e5c97f85372afee28cf2b6059d7d3818919 - subpackages: - - context - - package: github.com/gorilla/handlers - ref: 40694b40f4a928c062f56849989d3e9cd0570e5f - - package: github.com/docker/libkv - ref: 3732f7ff1b56057c3158f10bceb1e79133025373 - - package: github.com/alecthomas/template - ref: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 - - package: github.com/vdemeester/shakers - ref: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3 - - package: github.com/alecthomas/units - ref: 6b4e7dc5e3143b85ea77909c72caf89416fc2915 - - package: github.com/gambol99/go-marathon - ref: ade11d1dc2884ee1f387078fc28509559b6235d1 - - package: github.com/vulcand/predicate - ref: cb0bff91a7ab7cf7571e661ff883fc997bc554a3 - - package: github.com/thoas/stats - ref: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8 - - package: github.com/Sirupsen/logrus - ref: 418b41d23a1bf978c06faea5313ba194650ac088 - - package: github.com/unrolled/render - ref: 26b4e3aac686940fe29521545afad9966ddfc80c - - package: github.com/flynn/go-shlex - ref: 3f9db97f856818214da2e1057f8ad84803971cff - - package: github.com/boltdb/bolt - ref: 51f99c862475898df9773747d3accd05a7ca33c1 - - package: gopkg.in/mgo.v2 - ref: 22287bab4379e1fbf6002fb4eb769888f3fb224c - subpackages: - - bson - - package: github.com/docker/docker - ref: f39987afe8d611407887b3094c03d6ba6a766a67 - subpackages: - - autogen - - api - - cliconfig - - daemon/network - - graph/tags - - image - - opts - - pkg/archive - - pkg/fileutils - - pkg/homedir - - pkg/httputils - - pkg/ioutils - - pkg/jsonmessage - - pkg/mflag - - pkg/nat - - pkg/parsers - - pkg/pools - - pkg/promise - - pkg/random - - pkg/stdcopy - - pkg/stringid - - pkg/symlink - - pkg/system - - pkg/tarsum - - pkg/term - - pkg/timeutils - - pkg/tlsconfig - - pkg/ulimit - - pkg/units - - pkg/urlutil - - pkg/useragent - - pkg/version - - registry - - runconfig - - utils - - volume - - package: github.com/mailgun/timetools - ref: fd192d755b00c968d312d23f521eb0cdc6f66bd0 - - package: github.com/codegangsta/negroni - ref: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b - - package: gopkg.in/yaml.v2 - ref: 7ad95dd0798a40da1ccdff6dff35fd177b5edf40 - - package: github.com/opencontainers/runc - ref: 4ab132458fc3e9dbeea624153e0331952dc4c8d5 - subpackages: - - libcontainer/user - - package: github.com/gorilla/mux - ref: f15e0c49460fd49eebe2bcc8486b05d1bef68d3a - - package: github.com/BurntSushi/ty - ref: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74 - - package: github.com/elazarl/go-bindata-assetfs - ref: d5cac425555ca5cf00694df246e04f05e6a55150 - - package: github.com/BurntSushi/toml - ref: bd2bdf7f18f849530ef7a1c29a4290217cab32a1 - - package: gopkg.in/alecthomas/kingpin.v2 - ref: 639879d6110b1b0409410c7b737ef0bb18325038 - - package: github.com/cenkalti/backoff - ref: 4dc77674aceaabba2c7e3da25d4c823edfb73f99 - - package: gopkg.in/fsnotify.v1 - ref: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0 - - package: github.com/mailgun/manners - ref: fada45142db3f93097ca917da107aa3fad0ffcb5 - - package: github.com/gorilla/context - ref: 215affda49addc4c8ef7e2534915df2c8c35c6cd - - package: github.com/codahale/hdrhistogram - ref: 954f16e8b9ef0e5d5189456aa4c1202758e04f17 - - package: github.com/gorilla/websocket - - package: github.com/donovanhide/eventsource - ref: d8a3071799b98cacd30b6da92f536050ccfe6da4 - - package: github.com/golang/glog - ref: fca8c8854093a154ff1eb580aae10276ad6b1b5f - - package: github.com/spf13/cast - ref: ee7b3e0353166ab1f3a605294ac8cd2b77953778 - - package: github.com/mitchellh/mapstructure - - package: github.com/spf13/jwalterweatherman - - package: github.com/spf13/pflag - - package: github.com/wendal/errors - - package: github.com/hashicorp/hcl - - package: github.com/kr/pretty - - package: github.com/magiconair/properties - - package: github.com/kr/text - - package: github.com/spf13/viper - ref: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325 - - package: github.com/spf13/cobra - subpackages: - - /cobra - - package: github.com/google/go-querystring/query - - package: github.com/vulcand/vulcand/plugin/rewrite - - package: github.com/stretchr/testify/mock - - package: github.com/xenolf/lego - - package: github.com/vdemeester/libkermit - ref: 7e4e689a6fa9281e0fb9b7b9c297e22d5342a5ec - - package: github.com/docker/libcompose - version: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a - - package: github.com/docker/distribution - version: ff6f38ccb69afa96214c7ee955359465d1fc767a - subpackages: - - reference - - package: github.com/docker/engine-api - subpackages: - - client - - types - - types/container - - types/filters - - types/strslice - - package: github.com/vdemeester/docker-events - - package: github.com/docker/go-connections - subpackages: - - nat - - sockets - - tlsconfig - - package: github.com/docker/go-units - - package: github.com/mailgun/multibuf - - package: github.com/streamrail/concurrent-map +- package: github.com/coreos/go-etcd + version: cc90c7b091275e606ad0ca7102a23fb2072f3f5e + subpackages: + - etcd +- package: github.com/mailgun/log + version: 44874009257d4d47ba9806f1b7f72a32a015e4d8 +- package: github.com/containous/oxy + version: 021f82bd8260ba15f5862a9fe62018437720dff5 + subpackages: + - cbreaker + - forward + - memmetrics + - roundrobin + - utils +- package: github.com/hashicorp/consul + version: de080672fee9e6104572eeea89eccdca135bb918 + subpackages: + - api +- package: github.com/samuel/go-zookeeper + version: fa6674abf3f4580b946a01bf7a1ce4ba8766205b + subpackages: + - zk +- package: github.com/docker/libtrust + version: 9cbd2a1374f46905c68a4eb3694a130610adc62a +- package: github.com/go-check/check + version: 11d3bc7aa68e238947792f30573146a3231fc0f1 +- package: golang.org/x/net + version: d9558e5c97f85372afee28cf2b6059d7d3818919 + subpackages: + - context +- package: github.com/gorilla/handlers + version: 40694b40f4a928c062f56849989d3e9cd0570e5f +- package: github.com/docker/libkv + version: 3732f7ff1b56057c3158f10bceb1e79133025373 +- package: github.com/alecthomas/template + version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 +- package: github.com/vdemeester/shakers + version: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3 +- package: github.com/alecthomas/units + version: 6b4e7dc5e3143b85ea77909c72caf89416fc2915 +- package: github.com/gambol99/go-marathon + version: ade11d1dc2884ee1f387078fc28509559b6235d1 +- package: github.com/vulcand/predicate + version: cb0bff91a7ab7cf7571e661ff883fc997bc554a3 +- package: github.com/thoas/stats + version: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8 +- package: github.com/Sirupsen/logrus + version: 418b41d23a1bf978c06faea5313ba194650ac088 +- package: github.com/unrolled/render + version: 26b4e3aac686940fe29521545afad9966ddfc80c +- package: github.com/flynn/go-shlex + version: 3f9db97f856818214da2e1057f8ad84803971cff +- package: github.com/boltdb/bolt + version: 51f99c862475898df9773747d3accd05a7ca33c1 +- package: gopkg.in/mgo.v2 + version: 22287bab4379e1fbf6002fb4eb769888f3fb224c + subpackages: + - bson +- package: github.com/docker/docker + version: f39987afe8d611407887b3094c03d6ba6a766a67 + subpackages: + - autogen + - api + - cliconfig + - daemon/network + - graph/tags + - image + - opts + - pkg/archive + - pkg/fileutils + - pkg/homedir + - pkg/httputils + - pkg/ioutils + - pkg/jsonmessage + - pkg/mflag + - pkg/nat + - pkg/parsers + - pkg/pools + - pkg/promise + - pkg/random + - pkg/stdcopy + - pkg/stringid + - pkg/symlink + - pkg/system + - pkg/tarsum + - pkg/term + - pkg/timeutils + - pkg/tlsconfig + - pkg/ulimit + - pkg/units + - pkg/urlutil + - pkg/useragent + - pkg/version + - registry + - runconfig + - utils + - volume +- package: github.com/mailgun/timetools + version: fd192d755b00c968d312d23f521eb0cdc6f66bd0 +- package: github.com/codegangsta/negroni + version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b +- package: gopkg.in/yaml.v2 + version: 7ad95dd0798a40da1ccdff6dff35fd177b5edf +- package: github.com/opencontainers/runc + version: 4ab132458fc3e9dbeea624153e0331952dc4c8d5 + subpackages: + - libcontainer/user +- package: github.com/gorilla/mux + version: f15e0c49460fd49eebe2bcc8486b05d1bef68d3a +- package: github.com/BurntSushi/ty + version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74 +- package: github.com/elazarl/go-bindata-assetfs + version: d5cac425555ca5cf00694df246e04f05e6a55150 +- package: github.com/BurntSushi/toml + version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f +- package: gopkg.in/alecthomas/kingpin.v2 + version: 639879d6110b1b0409410c7b737ef0bb18325038 +- package: github.com/cenkalti/backoff + version: 4dc77674aceaabba2c7e3da25d4c823edfb73f99 +- package: gopkg.in/fsnotify.v1 + version: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0 +- package: github.com/mailgun/manners + version: fada45142db3f93097ca917da107aa3fad0ffcb5 +- package: github.com/gorilla/context + version: 215affda49addc4c8ef7e2534915df2c8c35c6cd +- package: github.com/codahale/hdrhistogram + version: 954f16e8b9ef0e5d5189456aa4c1202758e04f17 +- package: github.com/gorilla/websocket +- package: github.com/donovanhide/eventsource + version: d8a3071799b98cacd30b6da92f536050ccfe6da4 +- package: github.com/golang/glog + version: fca8c8854093a154ff1eb580aae10276ad6b1b5f +- package: github.com/spf13/cast + version: ee7b3e0353166ab1f3a605294ac8cd2b77953778 +- package: github.com/mitchellh/mapstructure +- package: github.com/spf13/jwalterweatherman +- package: github.com/spf13/pflag +- package: github.com/wendal/errors +- package: github.com/hashicorp/hcl +- package: github.com/kr/pretty +- package: github.com/magiconair/properties +- package: github.com/kr/text +- package: github.com/spf13/viper + version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325 +- package: github.com/spf13/cobra + subpackages: + - cobra +- package: github.com/google/go-querystring + subpackages: + - query +- package: github.com/vulcand/vulcand + subpackages: + - plugin/rewrite +- package: github.com/stretchr/testify + subpackages: + - mock +- package: github.com/xenolf/lego +- package: github.com/vdemeester/libkermit + version: 7e4e689a6fa9281e0fb9b7b9c297e22d5342a5ec +- package: github.com/docker/libcompose + version: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a +- package: github.com/docker/distribution + version: ff6f38ccb69afa96214c7ee955359465d1fc767a + subpackages: + - reference +- package: github.com/docker/engine-api + version: 8924d6900370b4c7e7984be5adc61f50a80d7537 + subpackages: + - client + - types + - types/container + - types/filters + - types/strslice +- package: github.com/vdemeester/docker-events +- package: github.com/docker/go-connections + subpackages: + - nat + - sockets + - tlsconfig +- package: github.com/docker/go-units +- package: github.com/mailgun/multibuf +- package: github.com/streamrail/concurrent-map +- package: github.com/parnurzeal/gorequest diff --git a/integration/basic_test.go b/integration/basic_test.go index be2fc6c20..3a4b47a25 100644 --- a/integration/basic_test.go +++ b/integration/basic_test.go @@ -19,14 +19,14 @@ func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) { output, err := cmd.CombinedOutput() c.Assert(err, checker.NotNil) - c.Assert(string(output), checker.Contains, "Error reading file: open : no such file or directory") + c.Assert(string(output), checker.Contains, "No configuration file found") nonExistentFile := "non/existent/file.toml" cmd = exec.Command(traefikBinary, "--configFile="+nonExistentFile) output, err = cmd.CombinedOutput() c.Assert(err, checker.NotNil) - c.Assert(string(output), checker.Contains, fmt.Sprintf("Error reading file: open %s: no such file or directory", nonExistentFile)) + c.Assert(string(output), checker.Contains, fmt.Sprintf("Error reading configuration file: open %s: no such file or directory", nonExistentFile)) } func (s *SimpleSuite) TestInvalidConfigShouldFail(c *check.C) { @@ -34,7 +34,7 @@ func (s *SimpleSuite) TestInvalidConfigShouldFail(c *check.C) { output, err := cmd.CombinedOutput() c.Assert(err, checker.NotNil) - c.Assert(string(output), checker.Contains, "Error reading file: While parsing config: Near line 1") + c.Assert(string(output), checker.Contains, "While parsing config: Near line 0 (last key parsed ''): Bare keys cannot contain '{'") } func (s *SimpleSuite) TestSimpleDefaultConfig(c *check.C) { diff --git a/provider/docker_test.go b/provider/docker_test.go index 175b83d0a..ec0b15254 100644 --- a/provider/docker_test.go +++ b/provider/docker_test.go @@ -743,11 +743,11 @@ func TestDockerLoadDockerConfig(t *testing.T) { }, }, expectedFrontends: map[string]*types.Frontend{ - `"frontend-Host-test-docker-localhost"`: { + "frontend-Host-test-docker-localhost": { Backend: "backend-test", EntryPoints: []string{}, Routes: map[string]types.Route{ - `"route-frontend-Host-test-docker-localhost"`: { + "route-frontend-Host-test-docker-localhost": { Rule: "Host:test.docker.localhost", }, }, @@ -815,20 +815,20 @@ func TestDockerLoadDockerConfig(t *testing.T) { }, }, expectedFrontends: map[string]*types.Frontend{ - `"frontend-Host-test1-docker-localhost"`: { + "frontend-Host-test1-docker-localhost": { Backend: "backend-foobar", EntryPoints: []string{"http", "https"}, Routes: map[string]types.Route{ - `"route-frontend-Host-test1-docker-localhost"`: { + "route-frontend-Host-test1-docker-localhost": { Rule: "Host:test1.docker.localhost", }, }, }, - `"frontend-Host-test2-docker-localhost"`: { + "frontend-Host-test2-docker-localhost": { Backend: "backend-foobar", EntryPoints: []string{}, Routes: map[string]types.Route{ - `"route-frontend-Host-test2-docker-localhost"`: { + "route-frontend-Host-test2-docker-localhost": { Rule: "Host:test2.docker.localhost", }, }, diff --git a/provider/k8s/client.go b/provider/k8s/client.go new file mode 100644 index 000000000..f1cbc9ddb --- /dev/null +++ b/provider/k8s/client.go @@ -0,0 +1,163 @@ +package k8s + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "github.com/containous/traefik/safe" + "github.com/parnurzeal/gorequest" + "net/http" + "net/url" + "strings" +) + +const ( + // APIEndpoint defines the base path for kubernetes API resources. + APIEndpoint = "/api/v1" + defaultService = "/namespaces/default/services" + extentionsEndpoint = "/apis/extensions/v1beta1" + defaultIngress = "/ingresses" +) + +// Client is a client for the Kubernetes master. +type Client struct { + endpointURL string + tls *tls.Config + token string + caCert []byte +} + +// NewClient returns a new Kubernetes client. +// The provided host is an url (scheme://hostname[:port]) of a +// Kubernetes master without any path. +// The provided client is an authorized http.Client used to perform requests to the Kubernetes API master. +func NewClient(baseURL string, caCert []byte, token string) (*Client, error) { + validURL, err := url.Parse(baseURL) + if err != nil { + return nil, fmt.Errorf("failed to parse URL %q: %v", baseURL, err) + } + return &Client{ + endpointURL: strings.TrimSuffix(validURL.String(), "/"), + token: token, + caCert: caCert, + }, nil +} + +// GetIngresses returns all services in the cluster +func (c *Client) GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) { + getURL := c.endpointURL + extentionsEndpoint + defaultIngress + request := gorequest.New().Get(getURL) + if len(c.token) > 0 { + request.Header["Authorization"] = "Bearer " + c.token + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(c.caCert) + c.tls = &tls.Config{RootCAs: pool} + } + res, body, errs := request.TLSClientConfig(c.tls).EndBytes() + if errs != nil { + return nil, fmt.Errorf("failed to create request: GET %q : %v", getURL, errs) + } + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("http error %d GET %q: %q", res.StatusCode, getURL, string(body)) + } + + var ingressList IngressList + if err := json.Unmarshal(body, &ingressList); err != nil { + return nil, fmt.Errorf("failed to decode list of ingress resources: %v", err) + } + ingresses := ingressList.Items[:0] + for _, ingress := range ingressList.Items { + if predicate(ingress) { + ingresses = append(ingresses, ingress) + } + } + return ingresses, nil +} + +// WatchIngresses returns all services in the cluster +func (c *Client) WatchIngresses(predicate func(Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) { + watchCh := make(chan interface{}) + errCh := make(chan error) + + getURL := c.endpointURL + extentionsEndpoint + defaultIngress + "?watch=true" + + // Make request to Kubernetes API + request := gorequest.New().Get(getURL) + if len(c.token) > 0 { + request.Set("Authorization", "Bearer "+c.token) + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(c.caCert) + c.tls = &tls.Config{RootCAs: pool} + } + req, err := request.TLSClientConfig(c.tls).MakeRequest() + if err != nil { + return watchCh, errCh, fmt.Errorf("failed to create request: GET %q : %v", getURL, err) + } + request.Client.Transport = request.Transport + res, err := request.Client.Do(req) + if err != nil { + return watchCh, errCh, fmt.Errorf("failed to make request: GET %q: %v", getURL, err) + } + + shouldStop := safe.New(false) + + go func() { + select { + case <-stopCh: + shouldStop.Set(true) + res.Body.Close() + return + } + }() + + go func() { + defer close(watchCh) + defer close(errCh) + for { + var ingressList interface{} + if err := json.NewDecoder(res.Body).Decode(&ingressList); err != nil { + if !shouldStop.Get().(bool) { + errCh <- fmt.Errorf("failed to decode list of ingress resources: %v", err) + } + return + } + + watchCh <- ingressList + } + }() + return watchCh, errCh, nil +} + +// GetServices returns all services in the cluster +func (c *Client) GetServices(predicate func(Service) bool) ([]Service, error) { + getURL := c.endpointURL + APIEndpoint + defaultService + + // Make request to Kubernetes API + request := gorequest.New().Get(getURL) + if len(c.token) > 0 { + request.Header["Authorization"] = "Bearer " + c.token + pool := x509.NewCertPool() + pool.AppendCertsFromPEM(c.caCert) + c.tls = &tls.Config{RootCAs: pool} + } + res, body, errs := request.TLSClientConfig(c.tls).EndBytes() + if errs != nil { + return nil, fmt.Errorf("failed to create request: GET %q : %v", getURL, errs) + } + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("http error %d GET %q: %q", res.StatusCode, getURL, string(body)) + } + + var serviceList ServiceList + if err := json.Unmarshal(body, &serviceList); err != nil { + return nil, fmt.Errorf("failed to decode list of services resources: %v", err) + } + services := serviceList.Items[:0] + for _, service := range serviceList.Items { + if predicate(service) { + services = append(services, service) + } + } + return services, nil +} diff --git a/provider/k8s/ingress.go b/provider/k8s/ingress.go new file mode 100644 index 000000000..f3b7c8dce --- /dev/null +++ b/provider/k8s/ingress.go @@ -0,0 +1,151 @@ +package k8s + +// Ingress is a collection of rules that allow inbound connections to reach the +// endpoints defined by a backend. An Ingress can be configured to give services +// externally-reachable urls, load balance traffic, terminate SSL, offer name +// based virtual hosting etc. +type Ingress struct { + TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata + ObjectMeta `json:"metadata,omitempty"` + + // Spec is the desired state of the Ingress. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status + Spec IngressSpec `json:"spec,omitempty"` + + // Status is the current state of the Ingress. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status + Status IngressStatus `json:"status,omitempty"` +} + +// IngressList is a collection of Ingress. +type IngressList struct { + TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata + ListMeta `json:"metadata,omitempty"` + + // Items is the list of Ingress. + Items []Ingress `json:"items"` +} + +// IngressSpec describes the Ingress the user wishes to exist. +type IngressSpec struct { + // A default backend capable of servicing requests that don't match any + // rule. At least one of 'backend' or 'rules' must be specified. This field + // is optional to allow the loadbalancer controller or defaulting logic to + // specify a global default. + Backend *IngressBackend `json:"backend,omitempty"` + + // TLS configuration. Currently the Ingress only supports a single TLS + // port, 443. If multiple members of this list specify different hosts, they + // will be multiplexed on the same port according to the hostname specified + // through the SNI TLS extension, if the ingress controller fulfilling the + // ingress supports SNI. + TLS []IngressTLS `json:"tls,omitempty"` + + // A list of host rules used to configure the Ingress. If unspecified, or + // no rule matches, all traffic is sent to the default backend. + Rules []IngressRule `json:"rules,omitempty"` + // TODO: Add the ability to specify load-balancer IP through claims +} + +// IngressTLS describes the transport layer security associated with an Ingress. +type IngressTLS struct { + // Hosts are a list of hosts included in the TLS certificate. The values in + // this list must match the name/s used in the tlsSecret. Defaults to the + // wildcard host setting for the loadbalancer controller fulfilling this + // Ingress, if left unspecified. + Hosts []string `json:"hosts,omitempty"` + // SecretName is the name of the secret used to terminate SSL traffic on 443. + // Field is left optional to allow SSL routing based on SNI hostname alone. + // If the SNI host in a listener conflicts with the "Host" header field used + // by an IngressRule, the SNI host is used for termination and value of the + // Host header is used for routing. + SecretName string `json:"secretName,omitempty"` + // TODO: Consider specifying different modes of termination, protocols etc. +} + +// IngressStatus describe the current state of the Ingress. +type IngressStatus struct { + // LoadBalancer contains the current status of the load-balancer. + LoadBalancer LoadBalancerStatus `json:"loadBalancer,omitempty"` +} + +// IngressRule represents the rules mapping the paths under a specified host to +// the related backend services. Incoming requests are first evaluated for a host +// match, then routed to the backend associated with the matching IngressRuleValue. +type IngressRule struct { + // Host is the fully qualified domain name of a network host, as defined + // by RFC 3986. Note the following deviations from the "host" part of the + // URI as defined in the RFC: + // 1. IPs are not allowed. Currently an IngressRuleValue can only apply to the + // IP in the Spec of the parent Ingress. + // 2. The `:` delimiter is not respected because ports are not allowed. + // Currently the port of an Ingress is implicitly :80 for http and + // :443 for https. + // Both these may change in the future. + // Incoming requests are matched against the host before the IngressRuleValue. + // If the host is unspecified, the Ingress routes all traffic based on the + // specified IngressRuleValue. + Host string `json:"host,omitempty"` + // IngressRuleValue represents a rule to route requests for this IngressRule. + // If unspecified, the rule defaults to a http catch-all. Whether that sends + // just traffic matching the host to the default backend or all traffic to the + // default backend, is left to the controller fulfilling the Ingress. Http is + // currently the only supported IngressRuleValue. + IngressRuleValue `json:",inline,omitempty"` +} + +// IngressRuleValue represents a rule to apply against incoming requests. If the +// rule is satisfied, the request is routed to the specified backend. Currently +// mixing different types of rules in a single Ingress is disallowed, so exactly +// one of the following must be set. +type IngressRuleValue struct { + //TODO: + // 1. Consider renaming this resource and the associated rules so they + // aren't tied to Ingress. They can be used to route intra-cluster traffic. + // 2. Consider adding fields for ingress-type specific global options + // usable by a loadbalancer, like http keep-alive. + + HTTP *HTTPIngressRuleValue `json:"http,omitempty"` +} + +// HTTPIngressRuleValue is a list of http selectors pointing to backends. +// In the example: http:///? -> backend where +// where parts of the url correspond to RFC 3986, this resource will be used +// to match against everything after the last '/' and before the first '?' +// or '#'. +type HTTPIngressRuleValue struct { + // A collection of paths that map requests to backends. + Paths []HTTPIngressPath `json:"paths"` + // TODO: Consider adding fields for ingress-type specific global + // options usable by a loadbalancer, like http keep-alive. +} + +// HTTPIngressPath associates a path regex with a backend. Incoming urls matching +// the path are forwarded to the backend. +type HTTPIngressPath struct { + // Path is a extended POSIX regex as defined by IEEE Std 1003.1, + // (i.e this follows the egrep/unix syntax, not the perl syntax) + // matched against the path of an incoming request. Currently it can + // contain characters disallowed from the conventional "path" + // part of a URL as defined by RFC 3986. Paths must begin with + // a '/'. If unspecified, the path defaults to a catch all sending + // traffic to the backend. + Path string `json:"path,omitempty"` + + // Backend defines the referenced service endpoint to which the traffic + // will be forwarded to. + Backend IngressBackend `json:"backend"` +} + +// IngressBackend describes all endpoints for a given service and port. +type IngressBackend struct { + // Specifies the name of the referenced service. + ServiceName string `json:"serviceName"` + + // Specifies the port of the referenced service. + ServicePort IntOrString `json:"servicePort"` +} diff --git a/provider/k8s/service.go b/provider/k8s/service.go new file mode 100644 index 000000000..de5711ca3 --- /dev/null +++ b/provider/k8s/service.go @@ -0,0 +1,313 @@ +package k8s + +import ( + "encoding/json" + "strconv" + "time" +) + +// TypeMeta describes an individual object in an API response or request +// with strings representing the type of the object and its API schema version. +// Structures that are versioned or persisted should inline TypeMeta. +type TypeMeta struct { + // Kind is a string value representing the REST resource this object represents. + // Servers may infer this from the endpoint the client submits requests to. + // Cannot be updated. + // In CamelCase. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds + Kind string `json:"kind,omitempty"` + + // APIVersion defines the versioned schema of this representation of an object. + // Servers should convert recognized schemas to the latest internal value, and + // may reject unrecognized values. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#resources + APIVersion string `json:"apiVersion,omitempty"` +} + +// ObjectMeta is metadata that all persisted resources must have, which includes all objects +// users must create. +type ObjectMeta struct { + // Name is unique within a namespace. Name is required when creating resources, although + // some resources may allow a client to request the generation of an appropriate name + // automatically. Name is primarily intended for creation idempotence and configuration + // definition. + Name string `json:"name,omitempty"` + + // GenerateName indicates that the name should be made unique by the server prior to persisting + // it. A non-empty value for the field indicates the name will be made unique (and the name + // returned to the client will be different than the name passed). The value of this field will + // be combined with a unique suffix on the server if the Name field has not been provided. + // The provided value must be valid within the rules for Name, and may be truncated by the length + // of the suffix required to make the value unique on the server. + // + // If this field is specified, and Name is not present, the server will NOT return a 409 if the + // generated name exists - instead, it will either return 201 Created or 500 with Reason + // ServerTimeout indicating a unique name could not be found in the time allotted, and the client + // should retry (optionally after the time indicated in the Retry-After header). + GenerateName string `json:"generateName,omitempty"` + + // Namespace defines the space within which name must be unique. An empty namespace is + // equivalent to the "default" namespace, but "default" is the canonical representation. + // Not all objects are required to be scoped to a namespace - the value of this field for + // those objects will be empty. + Namespace string `json:"namespace,omitempty"` + + // SelfLink is a URL representing this object. + SelfLink string `json:"selfLink,omitempty"` + + // UID is the unique in time and space value for this object. It is typically generated by + // the server on successful creation of a resource and is not allowed to change on PUT + // operations. + UID UID `json:"uid,omitempty"` + + // An opaque value that represents the version of this resource. May be used for optimistic + // concurrency, change detection, and the watch operation on a resource or set of resources. + // Clients must treat these values as opaque and values may only be valid for a particular + // resource or set of resources. Only servers will generate resource versions. + ResourceVersion string `json:"resourceVersion,omitempty"` + + // A sequence number representing a specific generation of the desired state. + // Populated by the system. Read-only. + Generation int64 `json:"generation,omitempty"` + + // CreationTimestamp is a timestamp representing the server time when this object was + // created. It is not guaranteed to be set in happens-before order across separate operations. + // Clients may not set this value. It is represented in RFC3339 form and is in UTC. + CreationTimestamp Time `json:"creationTimestamp,omitempty"` + + // DeletionTimestamp is the time after which this resource will be deleted. This + // field is set by the server when a graceful deletion is requested by the user, and is not + // directly settable by a client. The resource will be deleted (no longer visible from + // resource lists, and not reachable by name) after the time in this field. Once set, this + // value may not be unset or be set further into the future, although it may be shortened + // or the resource may be deleted prior to this time. For example, a user may request that + // a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination + // signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet + // will send a hard termination signal to the container. + DeletionTimestamp *Time `json:"deletionTimestamp,omitempty"` + + // DeletionGracePeriodSeconds records the graceful deletion value set when graceful deletion + // was requested. Represents the most recent grace period, and may only be shortened once set. + DeletionGracePeriodSeconds *int64 `json:"deletionGracePeriodSeconds,omitempty"` + + // Labels are key value pairs that may be used to scope and select individual resources. + // Label keys are of the form: + // label-key ::= prefixed-name | name + // prefixed-name ::= prefix '/' name + // prefix ::= DNS_SUBDOMAIN + // name ::= DNS_LABEL + // The prefix is optional. If the prefix is not specified, the key is assumed to be private + // to the user. Other system components that wish to use labels must specify a prefix. The + // "kubernetes.io/" prefix is reserved for use by kubernetes components. + // TODO: replace map[string]string with labels.LabelSet type + Labels map[string]string `json:"labels,omitempty"` + + // Annotations are unstructured key value data stored with a resource that may be set by + // external tooling. They are not queryable and should be preserved when modifying + // objects. Annotation keys have the same formatting restrictions as Label keys. See the + // comments on Labels for details. + Annotations map[string]string `json:"annotations,omitempty"` +} + +// UID is a type that holds unique ID values, including UUIDs. Because we +// don't ONLY use UUIDs, this is an alias to string. Being a type captures +// intent and helps make sure that UIDs and names do not get conflated. +type UID string + +// Time is a wrapper around time.Time which supports correct +// marshaling to YAML and JSON. Wrappers are provided for many +// of the factory methods that the time package offers. +// +// +protobuf.options.marshal=false +// +protobuf.as=Timestamp +type Time struct { + time.Time `protobuf:"-"` +} + +// Service is a named abstraction of software service (for example, mysql) consisting of local port +// (for example 3306) that the proxy listens on, and the selector that determines which pods +// will answer requests sent through the proxy. +type Service struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the behavior of a service. + Spec ServiceSpec `json:"spec,omitempty"` + + // Status represents the current status of a service. + Status ServiceStatus `json:"status,omitempty"` +} + +// ServiceSpec describes the attributes that a user creates on a service +type ServiceSpec struct { + // Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer + Type ServiceType `json:"type,omitempty"` + + // Required: The list of ports that are exposed by this service. + Ports []ServicePort `json:"ports"` + + // This service will route traffic to pods having labels matching this selector. If empty or not present, + // the service is assumed to have endpoints set by an external process and Kubernetes will not modify + // those endpoints. + Selector map[string]string `json:"selector"` + + // ClusterIP is usually assigned by the master. If specified by the user + // we will try to respect it or else fail the request. This field can + // not be changed by updates. + // Valid values are None, empty string (""), or a valid IP address + // None can be specified for headless services when proxying is not required + ClusterIP string `json:"clusterIP,omitempty"` + + // ExternalIPs are used by external load balancers, or can be set by + // users to handle external traffic that arrives at a node. + ExternalIPs []string `json:"externalIPs,omitempty"` + + // Only applies to Service Type: LoadBalancer + // LoadBalancer will get created with the IP specified in this field. + // This feature depends on whether the underlying cloud-provider supports specifying + // the loadBalancerIP when a load balancer is created. + // This field will be ignored if the cloud-provider does not support the feature. + LoadBalancerIP string `json:"loadBalancerIP,omitempty"` + + // Required: Supports "ClientIP" and "None". Used to maintain session affinity. + SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty"` +} + +// ServicePort service port +type ServicePort struct { + // Optional if only one ServicePort is defined on this service: The + // name of this port within the service. This must be a DNS_LABEL. + // All ports within a ServiceSpec must have unique names. This maps to + // the 'Name' field in EndpointPort objects. + Name string `json:"name"` + + // The IP protocol for this port. Supports "TCP" and "UDP". + Protocol Protocol `json:"protocol"` + + // The port that will be exposed on the service. + Port int `json:"port"` + + // Optional: The target port on pods selected by this service. If this + // is a string, it will be looked up as a named port in the target + // Pod's container ports. If this is not specified, the value + // of the 'port' field is used (an identity map). + // This field is ignored for services with clusterIP=None, and should be + // omitted or set equal to the 'port' field. + TargetPort IntOrString `json:"targetPort"` + + // The port on each node on which this service is exposed. + // Default is to auto-allocate a port if the ServiceType of this Service requires one. + NodePort int `json:"nodePort"` +} + +// ServiceStatus represents the current status of a service +type ServiceStatus struct { + // LoadBalancer contains the current status of the load-balancer, + // if one is present. + LoadBalancer LoadBalancerStatus `json:"loadBalancer,omitempty"` +} + +// LoadBalancerStatus represents the status of a load-balancer +type LoadBalancerStatus struct { + // Ingress is a list containing ingress points for the load-balancer; + // traffic intended for the service should be sent to these ingress points. + Ingress []LoadBalancerIngress `json:"ingress,omitempty"` +} + +// LoadBalancerIngress represents the status of a load-balancer ingress point: +// traffic intended for the service should be sent to an ingress point. +type LoadBalancerIngress struct { + // IP is set for load-balancer ingress points that are IP based + // (typically GCE or OpenStack load-balancers) + IP string `json:"ip,omitempty"` + + // Hostname is set for load-balancer ingress points that are DNS based + // (typically AWS load-balancers) + Hostname string `json:"hostname,omitempty"` +} + +// ServiceAffinity Session Affinity Type string +type ServiceAffinity string + +// ServiceType Service Type string describes ingress methods for a service +type ServiceType string + +// Protocol defines network protocols supported for things like container ports. +type Protocol string + +// IntOrString is a type that can hold an int32 or a string. When used in +// JSON or YAML marshalling and unmarshalling, it produces or consumes the +// inner type. This allows you to have, for example, a JSON field that can +// accept a name or number. +// TODO: Rename to Int32OrString +// +// +protobuf=true +// +protobuf.options.(gogoproto.goproto_stringer)=false +type IntOrString struct { + Type Type + IntVal int32 + StrVal string +} + +// String returns the string value, or the Itoa of the int value. +func (intstr *IntOrString) String() string { + if intstr.Type == String { + return intstr.StrVal + } + return strconv.Itoa(intstr.IntValue()) +} + +// IntValue returns the IntVal if type Int, or if +// it is a String, will attempt a conversion to int. +func (intstr *IntOrString) IntValue() int { + if intstr.Type == String { + i, _ := strconv.Atoi(intstr.StrVal) + return i + } + return int(intstr.IntVal) +} + +// UnmarshalJSON implements the json.Unmarshaller interface. +func (intstr *IntOrString) UnmarshalJSON(value []byte) error { + if value[0] == '"' { + intstr.Type = String + return json.Unmarshal(value, &intstr.StrVal) + } + intstr.Type = Int + return json.Unmarshal(value, &intstr.IntVal) +} + +// Type represents the stored type of IntOrString. +type Type int + +const ( + // Int int + Int Type = iota // The IntOrString holds an int. + //String string + String // The IntOrString holds a string. +) + +// ServiceList holds a list of services. +type ServiceList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty"` + + Items []Service `json:"items"` +} + +// ListMeta describes metadata that synthetic resources must have, including lists and +// various status objects. A resource may have only one of {ObjectMeta, ListMeta}. +type ListMeta struct { + // SelfLink is a URL representing this object. + // Populated by the system. + // Read-only. + SelfLink string `json:"selfLink,omitempty"` + + // String that identifies the server's internal version of this object that + // can be used by clients to determine when objects have changed. + // Value must be treated as opaque by clients and passed unmodified back to the server. + // Populated by the system. + // Read-only. + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#concurrency-control-and-consistency + ResourceVersion string `json:"resourceVersion,omitempty"` +} diff --git a/provider/kubernetes.go b/provider/kubernetes.go new file mode 100644 index 000000000..133618041 --- /dev/null +++ b/provider/kubernetes.go @@ -0,0 +1,164 @@ +package provider + +import ( + log "github.com/Sirupsen/logrus" + "github.com/cenkalti/backoff" + "github.com/containous/traefik/provider/k8s" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" + "io/ioutil" + "os" + "text/template" + "time" +) + +const ( + serviceAccountToken = "/var/run/secrets/kubernetes.io/serviceaccount/token" + serviceAccountCACert = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" +) + +// Kubernetes holds configurations of the Kubernetes provider. +type Kubernetes struct { + BaseProvider `mapstructure:",squash"` + Endpoint string +} + +// Provide allows the provider to provide configurations to traefik +// using the given configuration channel. +func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { + var token string + tokenBytes, err := ioutil.ReadFile(serviceAccountToken) + if err == nil { + token = string(tokenBytes) + log.Debugf("Kubernetes token: %s", token) + } else { + log.Debugf("Kubernetes load token error: %s", err) + } + caCert, err := ioutil.ReadFile(serviceAccountCACert) + if err == nil { + log.Debugf("Kubernetes CA cert: %s", serviceAccountCACert) + } else { + log.Debugf("Kubernetes load token error: %s", err) + } + kubernetesHost := os.Getenv("KUBERNETES_SERVICE_HOST") + kubernetesPort := os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS") + if len(kubernetesPort) > 0 && len(kubernetesHost) > 0 { + provider.Endpoint = "https://" + kubernetesHost + ":" + kubernetesPort + } + log.Debugf("Kubernetes endpoint: %s", provider.Endpoint) + k8sClient, err := k8s.NewClient(provider.Endpoint, caCert, token) + if err != nil { + return err + } + + pool.Go(func(stop chan bool) { + stopWatch := make(chan bool) + operation := func() error { + select { + case <-stop: + return nil + default: + } + ingressesChan, errChan, err := k8sClient.WatchIngresses(func(ingress k8s.Ingress) bool { + return true + }, stopWatch) + if err != nil { + log.Errorf("Error retrieving ingresses: %v", err) + return err + } + for { + templateObjects := types.Configuration{ + map[string]*types.Backend{}, + map[string]*types.Frontend{}, + } + select { + case <-stop: + stopWatch <- true + return nil + case err := <-errChan: + return err + case event := <-ingressesChan: + log.Debugf("Received event from kubenetes %+v", event) + ingresses, err := k8sClient.GetIngresses(func(ingress k8s.Ingress) bool { + return true + }) + if err != nil { + log.Errorf("Error retrieving ingresses: %+v", err) + continue + } + for _, i := range ingresses { + for _, r := range i.Spec.Rules { + for _, pa := range r.HTTP.Paths { + if _, exists := templateObjects.Backends[r.Host+pa.Path]; !exists { + templateObjects.Backends[r.Host+pa.Path] = &types.Backend{ + Servers: make(map[string]types.Server), + } + } + if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists { + templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{ + Backend: r.Host + pa.Path, + Routes: make(map[string]types.Route), + } + } + if _, exists := templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host]; !exists { + templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host] = types.Route{ + Rule: "Host:" + r.Host, + } + } + if len(pa.Path) > 0 { + templateObjects.Frontends[r.Host+pa.Path].Routes[pa.Path] = types.Route{ + Rule: "Path:" + pa.Path, + } + } + services, err := k8sClient.GetServices(func(service k8s.Service) bool { + return service.Name == pa.Backend.ServiceName + }) + if err != nil { + log.Errorf("Error retrieving services: %v", err) + continue + } + for _, service := range services { + var protocol string + for _, port := range service.Spec.Ports { + if port.Port == pa.Backend.ServicePort.IntValue() { + protocol = port.Name + break + } + } + templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{ + URL: protocol + "://" + service.Spec.ClusterIP + ":" + pa.Backend.ServicePort.String(), + Weight: 1, + } + } + } + } + } + + configurationChan <- types.ConfigMessage{ + ProviderName: "kubernetes", + Configuration: provider.loadConfig(templateObjects), + } + } + } + } + + notify := func(err error, time time.Duration) { + log.Errorf("Kubernetes connection error %+v, retrying in %s", err, time) + } + err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify) + if err != nil { + log.Fatalf("Cannot connect to Kubernetes server %+v", err) + } + }) + + return nil +} + +func (provider *Kubernetes) loadConfig(templateObjects types.Configuration) *types.Configuration { + var FuncMap = template.FuncMap{} + configuration, err := provider.getConfiguration("templates/kubernetes.tmpl", FuncMap, templateObjects) + if err != nil { + log.Error(err) + } + return configuration +} diff --git a/provider/provider_test.go b/provider/provider_test.go index 70da81893..b76f5e6bd 100644 --- a/provider/provider_test.go +++ b/provider/provider_test.go @@ -74,7 +74,7 @@ func TestConfigurationErrors(t *testing.T) { Filename: templateInvalidTOMLFile.Name(), }, }, - expectedError: "Near line 1, key 'Hello': Near line 1: Expected key separator '=', but got '<' instead", + expectedError: "Near line 1 (last key parsed 'Hello'): Expected key separator '=', but got '<' instead", funcMap: template.FuncMap{ "Foo": func() string { return "bar" diff --git a/script/binary b/script/binary index c2451e801..fe69c9d56 100755 --- a/script/binary +++ b/script/binary @@ -22,4 +22,4 @@ if [ -z "$DATE" ]; then fi # Build binaries -CGO_ENABLED=0 GOGC=off go build $FLAGS -ldflags "-X main.Version=$VERSION -X main.BuildDate=$DATE" -a -installsuffix nocgo -o dist/traefik . +CGO_ENABLED=0 GOGC=off go build $FLAGS -ldflags "-s -w -X main.Version=$VERSION -X main.BuildDate=$DATE" -a -installsuffix nocgo -o dist/traefik . diff --git a/server.go b/server.go index fc948c462..77d558947 100644 --- a/server.go +++ b/server.go @@ -236,6 +236,9 @@ func (server *Server) configureProviders() { if server.globalConfiguration.Boltdb != nil { server.providers = append(server.providers, server.globalConfiguration.Boltdb) } + if server.globalConfiguration.Kubernetes != nil { + server.providers = append(server.providers, server.globalConfiguration.Kubernetes) + } } func (server *Server) startProviders() { diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl new file mode 100644 index 000000000..1f7dfaba1 --- /dev/null +++ b/templates/kubernetes.tmpl @@ -0,0 +1,16 @@ +[backends]{{range $backendName, $backend := .Backends}} + {{range $serverName, $server := $backend.Servers}} + [backends."{{$backendName}}".servers."{{$serverName}}"] + url = "{{$server.URL}}" + weight = {{$server.Weight}} + {{end}} +{{end}} + +[frontends]{{range $frontendName, $frontend := .Frontends}} + [frontends."{{$frontendName}}"] + backend = "{{$frontend.Backend}}" + {{range $routeName, $route := $frontend.Routes}} + [frontends."{{$frontendName}}".routes."{{$routeName}}"] + rule = "{{$route.Rule}}" + {{end}} +{{end}} From d82e1342fb3908b4644feddc5e9615c549ec9663 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Tue, 19 Apr 2016 19:23:08 +0200 Subject: [PATCH 05/15] Fix integration test Signed-off-by: Emile Vauge --- integration/basic_test.go | 33 +++++++-- provider/kubernetes.go | 135 +++++++++++++++++++----------------- provider/kubernetes_test.go | 1 + provider/kv_test.go | 92 ++++++++++++++++++++++-- templates/kubernetes.tmpl | 2 +- templates/kv.tmpl | 10 +-- 6 files changed, 194 insertions(+), 79 deletions(-) create mode 100644 provider/kubernetes_test.go diff --git a/integration/basic_test.go b/integration/basic_test.go index 3a4b47a25..40f5ab997 100644 --- a/integration/basic_test.go +++ b/integration/basic_test.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/go-check/check" + "bytes" checker "github.com/vdemeester/shakers" ) @@ -16,24 +17,44 @@ type SimpleSuite struct{ BaseSuite } func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) { cmd := exec.Command(traefikBinary) - output, err := cmd.CombinedOutput() - c.Assert(err, checker.NotNil) + var b bytes.Buffer + cmd.Stdout = &b + cmd.Stderr = &b + + cmd.Start() + time.Sleep(500 * time.Millisecond) + output := b.Bytes() + c.Assert(string(output), checker.Contains, "No configuration file found") + cmd.Process.Kill() nonExistentFile := "non/existent/file.toml" cmd = exec.Command(traefikBinary, "--configFile="+nonExistentFile) - output, err = cmd.CombinedOutput() - c.Assert(err, checker.NotNil) + cmd.Stdout = &b + cmd.Stderr = &b + + cmd.Start() + time.Sleep(500 * time.Millisecond) + output = b.Bytes() + c.Assert(string(output), checker.Contains, fmt.Sprintf("Error reading configuration file: open %s: no such file or directory", nonExistentFile)) + cmd.Process.Kill() } func (s *SimpleSuite) TestInvalidConfigShouldFail(c *check.C) { cmd := exec.Command(traefikBinary, "--configFile=fixtures/invalid_configuration.toml") - output, err := cmd.CombinedOutput() - c.Assert(err, checker.NotNil) + var b bytes.Buffer + cmd.Stdout = &b + cmd.Stderr = &b + + cmd.Start() + time.Sleep(500 * time.Millisecond) + defer cmd.Process.Kill() + output := b.Bytes() + c.Assert(string(output), checker.Contains, "While parsing config: Near line 0 (last key parsed ''): Bare keys cannot contain '{'") } diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 133618041..bef3cf4a7 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -23,9 +23,7 @@ type Kubernetes struct { Endpoint string } -// Provide allows the provider to provide configurations to traefik -// using the given configuration channel. -func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { +func (provider *Kubernetes) createClient() (*k8s.Client, error) { var token string tokenBytes, err := ioutil.ReadFile(serviceAccountToken) if err == nil { @@ -46,7 +44,13 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage provider.Endpoint = "https://" + kubernetesHost + ":" + kubernetesPort } log.Debugf("Kubernetes endpoint: %s", provider.Endpoint) - k8sClient, err := k8s.NewClient(provider.Endpoint, caCert, token) + return k8s.NewClient(provider.Endpoint, caCert, token) +} + +// Provide allows the provider to provide configurations to traefik +// using the given configuration channel. +func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error { + k8sClient, err := provider.createClient() if err != nil { return err } @@ -67,10 +71,6 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage return err } for { - templateObjects := types.Configuration{ - map[string]*types.Backend{}, - map[string]*types.Frontend{}, - } select { case <-stop: stopWatch <- true @@ -79,64 +79,13 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage return err case event := <-ingressesChan: log.Debugf("Received event from kubenetes %+v", event) - ingresses, err := k8sClient.GetIngresses(func(ingress k8s.Ingress) bool { - return true - }) + templateObjects, err := provider.loadIngresses(k8sClient) if err != nil { - log.Errorf("Error retrieving ingresses: %+v", err) - continue + return err } - for _, i := range ingresses { - for _, r := range i.Spec.Rules { - for _, pa := range r.HTTP.Paths { - if _, exists := templateObjects.Backends[r.Host+pa.Path]; !exists { - templateObjects.Backends[r.Host+pa.Path] = &types.Backend{ - Servers: make(map[string]types.Server), - } - } - if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists { - templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{ - Backend: r.Host + pa.Path, - Routes: make(map[string]types.Route), - } - } - if _, exists := templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host]; !exists { - templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host] = types.Route{ - Rule: "Host:" + r.Host, - } - } - if len(pa.Path) > 0 { - templateObjects.Frontends[r.Host+pa.Path].Routes[pa.Path] = types.Route{ - Rule: "Path:" + pa.Path, - } - } - services, err := k8sClient.GetServices(func(service k8s.Service) bool { - return service.Name == pa.Backend.ServiceName - }) - if err != nil { - log.Errorf("Error retrieving services: %v", err) - continue - } - for _, service := range services { - var protocol string - for _, port := range service.Spec.Ports { - if port.Port == pa.Backend.ServicePort.IntValue() { - protocol = port.Name - break - } - } - templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{ - URL: protocol + "://" + service.Spec.ClusterIP + ":" + pa.Backend.ServicePort.String(), - Weight: 1, - } - } - } - } - } - configurationChan <- types.ConfigMessage{ ProviderName: "kubernetes", - Configuration: provider.loadConfig(templateObjects), + Configuration: provider.loadConfig(*templateObjects), } } } @@ -154,6 +103,68 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage return nil } +func (provider *Kubernetes) loadIngresses(k8sClient *k8s.Client) (*types.Configuration, error) { + ingresses, err := k8sClient.GetIngresses(func(ingress k8s.Ingress) bool { + return true + }) + if err != nil { + log.Errorf("Error retrieving ingresses: %+v", err) + return nil, err + } + templateObjects := types.Configuration{ + map[string]*types.Backend{}, + map[string]*types.Frontend{}, + } + for _, i := range ingresses { + for _, r := range i.Spec.Rules { + for _, pa := range r.HTTP.Paths { + if _, exists := templateObjects.Backends[r.Host+pa.Path]; !exists { + templateObjects.Backends[r.Host+pa.Path] = &types.Backend{ + Servers: make(map[string]types.Server), + } + } + if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists { + templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{ + Backend: r.Host + pa.Path, + Routes: make(map[string]types.Route), + } + } + if _, exists := templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host]; !exists { + templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host] = types.Route{ + Rule: "Host:" + r.Host, + } + } + if len(pa.Path) > 0 { + templateObjects.Frontends[r.Host+pa.Path].Routes[pa.Path] = types.Route{ + Rule: pa.Path, + } + } + services, err := k8sClient.GetServices(func(service k8s.Service) bool { + return service.Name == pa.Backend.ServiceName + }) + if err != nil { + log.Errorf("Error retrieving services: %v", err) + continue + } + for _, service := range services { + var protocol string + for _, port := range service.Spec.Ports { + if port.Port == pa.Backend.ServicePort.IntValue() { + protocol = port.Name + break + } + } + templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{ + URL: protocol + "://" + service.Spec.ClusterIP + ":" + pa.Backend.ServicePort.String(), + Weight: 1, + } + } + } + } + } + return &templateObjects, nil +} + func (provider *Kubernetes) loadConfig(templateObjects types.Configuration) *types.Configuration { var FuncMap = template.FuncMap{} configuration, err := provider.getConfiguration("templates/kubernetes.tmpl", FuncMap, templateObjects) diff --git a/provider/kubernetes_test.go b/provider/kubernetes_test.go new file mode 100644 index 000000000..4f504f668 --- /dev/null +++ b/provider/kubernetes_test.go @@ -0,0 +1 @@ +package provider diff --git a/provider/kv_test.go b/provider/kv_test.go index 965c963ca..99df7a72e 100644 --- a/provider/kv_test.go +++ b/provider/kv_test.go @@ -7,7 +7,6 @@ import ( "testing" "time" - "github.com/containous/traefik/safe" "github.com/docker/libkv/store" "reflect" "sort" @@ -81,7 +80,7 @@ func TestKvList(t *testing.T) { }, }, keys: []string{"foo", "/baz/"}, - expected: []string{"foo/baz/biz", "foo/baz/1", "foo/baz/2"}, + expected: []string{"foo/baz/1", "foo/baz/2"}, }, } @@ -257,9 +256,9 @@ func TestKvWatchTree(t *testing.T) { } configChan := make(chan types.ConfigMessage) - safe.Go(func() { + go func() { provider.watchKv(configChan, "prefix", make(chan bool, 1)) - }) + }() select { case c1 := <-returnedChans: @@ -339,7 +338,7 @@ func (s *Mock) List(prefix string) ([]*store.KVPair, error) { } kv := []*store.KVPair{} for _, kvPair := range s.KVPairs { - if strings.HasPrefix(kvPair.Key, prefix) { + if strings.HasPrefix(kvPair.Key, prefix) && !strings.ContainsAny(strings.TrimPrefix(kvPair.Key, prefix), "/") { kv = append(kv, kvPair) } } @@ -365,3 +364,86 @@ func (s *Mock) AtomicDelete(key string, previous *store.KVPair) (bool, error) { func (s *Mock) Close() { return } + +func TestKVLoadConfig(t *testing.T) { + provider := &Kv{ + 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("1"), + }, + }, + }, + } + 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: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + }, + Frontends: map[string]*types.Frontend{ + "frontend.with.dot": { + Backend: "backend.with.dot.too", + PassHostHeader: false, + 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) + } +} diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 1f7dfaba1..01a21c73c 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -11,6 +11,6 @@ backend = "{{$frontend.Backend}}" {{range $routeName, $route := $frontend.Routes}} [frontends."{{$frontendName}}".routes."{{$routeName}}"] - rule = "{{$route.Rule}}" + rule = "PathStrip:{{$route.Rule}}" {{end}} {{end}} diff --git a/templates/kv.tmpl b/templates/kv.tmpl index edb641658..70f257990 100644 --- a/templates/kv.tmpl +++ b/templates/kv.tmpl @@ -1,19 +1,19 @@ {{$frontends := List .Prefix "/frontends/" }} {{$backends := List .Prefix "/backends/"}} -{{range $backends}} +[backends]{{range $backends}} {{$backend := .}} {{$servers := List $backend "/servers/" }} {{$circuitBreaker := Get "" . "/circuitbreaker/" "expression"}} {{with $circuitBreaker}} -[backends.{{Last $backend}}.circuitBreaker] +[backends."{{Last $backend}}".circuitBreaker] expression = "{{$circuitBreaker}}" {{end}} {{$loadBalancer := Get "" . "/loadbalancer/" "method"}} {{with $loadBalancer}} -[backends.{{Last $backend}}.loadBalancer] +[backends."{{Last $backend}}".loadBalancer] method = "{{$loadBalancer}}" {{end}} @@ -21,14 +21,14 @@ {{$maxConnExtractorFunc := Get "" . "/maxconn/" "extractorfunc"}} {{with $maxConnAmt}} {{with $maxConnExtractorFunc}} -[backends.{{Last $backend}}.maxConn] +[backends."{{Last $backend}}".maxConn] amount = {{$maxConnAmt}} extractorFunc = "{{$maxConnExtractorFunc}}" {{end}} {{end}} {{range $servers}} -[backends.{{Last $backend}}.servers.{{Last .}}] +[backends."{{Last $backend}}".servers."{{Last .}}"] url = "{{Get "" . "/url"}}" weight = {{Get "" . "/weight"}} {{end}} From c0dd4c32098dc39b01267da3ea216ed695d624c8 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Wed, 20 Apr 2016 13:26:51 +0200 Subject: [PATCH 06/15] Add unit test Signed-off-by: Emile Vauge --- examples/k8s.ingress.yaml | 48 +++++----- examples/k8s.rc.yaml | 7 -- provider/k8s/client.go | 18 ++-- provider/k8s/service.go | 13 +++ provider/kubernetes.go | 14 +-- provider/kubernetes_test.go | 183 ++++++++++++++++++++++++++++++++++++ templates/kubernetes.tmpl | 2 +- webui/src/index.html | 4 +- 8 files changed, 242 insertions(+), 47 deletions(-) diff --git a/examples/k8s.ingress.yaml b/examples/k8s.ingress.yaml index 7963bb2a6..0e460f48f 100644 --- a/examples/k8s.ingress.yaml +++ b/examples/k8s.ingress.yaml @@ -2,14 +2,14 @@ apiVersion: v1 kind: Service metadata: - name: whoami-x + name: service1 labels: app: whoami spec: type: NodePort ports: - port: 80 - nodePort: 30301 + nodePort: 30283 targetPort: 80 protocol: TCP name: http @@ -19,24 +19,7 @@ spec: apiVersion: v1 kind: Service metadata: - name: whoami-default - labels: - app: whoami -spec: - type: NodePort - ports: - - port: 80 - nodePort: 30302 - targetPort: 80 - protocol: TCP - name: http - selector: - app: whoami ---- -apiVersion: v1 -kind: Service -metadata: - name: whoami-y + name: service2 labels: app: whoami spec: @@ -50,6 +33,23 @@ spec: selector: app: whoami --- +apiVersion: v1 +kind: Service +metadata: + name: service3 + labels: + app: whoami +spec: + type: NodePort + ports: + - port: 80 + nodePort: 30285 + targetPort: 80 + protocol: TCP + name: http + selector: + app: whoami +--- # A single RC matching all Services apiVersion: v1 kind: ReplicationController @@ -72,7 +72,7 @@ spec: apiVersion: extensions/v1beta1 kind: Ingress metadata: - name: whoamimap + name: whoamiIngress spec: rules: - host: foo.localhost @@ -80,14 +80,14 @@ spec: paths: - path: /bar backend: - serviceName: whoami-x + serviceName: service1 servicePort: 80 - host: bar.localhost http: paths: - backend: - serviceName: whoami-y + serviceName: service2 servicePort: 80 - backend: - serviceName: whoami-x + serviceName: service3 servicePort: 80 diff --git a/examples/k8s.rc.yaml b/examples/k8s.rc.yaml index 11963dfb2..74eb8aa62 100644 --- a/examples/k8s.rc.yaml +++ b/examples/k8s.rc.yaml @@ -19,13 +19,6 @@ spec: - image: containous/traefik:k8s name: traefik-ingress-lb imagePullPolicy: Always - # livenessProbe: - # httpGet: - # path: /healthz - # port: 10249 - # scheme: HTTP - # initialDelaySeconds: 30 - # timeoutSeconds: 5 ports: - containerPort: 80 hostPort: 80 diff --git a/provider/k8s/client.go b/provider/k8s/client.go index f1cbc9ddb..20e501cbb 100644 --- a/provider/k8s/client.go +++ b/provider/k8s/client.go @@ -21,7 +21,13 @@ const ( ) // Client is a client for the Kubernetes master. -type Client struct { +type Client interface { + GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) + WatchIngresses(predicate func(Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) + GetServices(predicate func(Service) bool) ([]Service, error) +} + +type clientImpl struct { endpointURL string tls *tls.Config token string @@ -32,12 +38,12 @@ type Client struct { // The provided host is an url (scheme://hostname[:port]) of a // Kubernetes master without any path. // The provided client is an authorized http.Client used to perform requests to the Kubernetes API master. -func NewClient(baseURL string, caCert []byte, token string) (*Client, error) { +func NewClient(baseURL string, caCert []byte, token string) (Client, error) { validURL, err := url.Parse(baseURL) if err != nil { return nil, fmt.Errorf("failed to parse URL %q: %v", baseURL, err) } - return &Client{ + return &clientImpl{ endpointURL: strings.TrimSuffix(validURL.String(), "/"), token: token, caCert: caCert, @@ -45,7 +51,7 @@ func NewClient(baseURL string, caCert []byte, token string) (*Client, error) { } // GetIngresses returns all services in the cluster -func (c *Client) GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) { +func (c *clientImpl) GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) { getURL := c.endpointURL + extentionsEndpoint + defaultIngress request := gorequest.New().Get(getURL) if len(c.token) > 0 { @@ -76,7 +82,7 @@ func (c *Client) GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) { } // WatchIngresses returns all services in the cluster -func (c *Client) WatchIngresses(predicate func(Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) { +func (c *clientImpl) WatchIngresses(predicate func(Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) { watchCh := make(chan interface{}) errCh := make(chan error) @@ -130,7 +136,7 @@ func (c *Client) WatchIngresses(predicate func(Ingress) bool, stopCh <-chan bool } // GetServices returns all services in the cluster -func (c *Client) GetServices(predicate func(Service) bool) ([]Service, error) { +func (c *clientImpl) GetServices(predicate func(Service) bool) ([]Service, error) { getURL := c.endpointURL + APIEndpoint + defaultService // Make request to Kubernetes API diff --git a/provider/k8s/service.go b/provider/k8s/service.go index de5711ca3..e501718ce 100644 --- a/provider/k8s/service.go +++ b/provider/k8s/service.go @@ -249,6 +249,19 @@ type IntOrString struct { StrVal string } +// FromInt creates an IntOrString object with an int32 value. It is +// your responsibility not to call this method with a value greater +// than int32. +// TODO: convert to (val int32) +func FromInt(val int) IntOrString { + return IntOrString{Type: Int, IntVal: int32(val)} +} + +// FromString creates an IntOrString object with a string value. +func FromString(val string) IntOrString { + return IntOrString{Type: String, StrVal: val} +} + // String returns the string value, or the Itoa of the int value. func (intstr *IntOrString) String() string { if intstr.Type == String { diff --git a/provider/kubernetes.go b/provider/kubernetes.go index bef3cf4a7..0ea5f06bb 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -23,7 +23,7 @@ type Kubernetes struct { Endpoint string } -func (provider *Kubernetes) createClient() (*k8s.Client, error) { +func (provider *Kubernetes) createClient() (k8s.Client, error) { var token string tokenBytes, err := ioutil.ReadFile(serviceAccountToken) if err == nil { @@ -103,7 +103,7 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage return nil } -func (provider *Kubernetes) loadIngresses(k8sClient *k8s.Client) (*types.Configuration, error) { +func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configuration, error) { ingresses, err := k8sClient.GetIngresses(func(ingress k8s.Ingress) bool { return true }) @@ -136,7 +136,7 @@ func (provider *Kubernetes) loadIngresses(k8sClient *k8s.Client) (*types.Configu } if len(pa.Path) > 0 { templateObjects.Frontends[r.Host+pa.Path].Routes[pa.Path] = types.Route{ - Rule: pa.Path, + Rule: "PathStrip:" + pa.Path, } } services, err := k8sClient.GetServices(func(service k8s.Service) bool { @@ -151,13 +151,13 @@ func (provider *Kubernetes) loadIngresses(k8sClient *k8s.Client) (*types.Configu for _, port := range service.Spec.Ports { if port.Port == pa.Backend.ServicePort.IntValue() { protocol = port.Name + templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{ + URL: protocol + "://" + service.Spec.ClusterIP + ":" + pa.Backend.ServicePort.String(), + Weight: 1, + } break } } - templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{ - URL: protocol + "://" + service.Spec.ClusterIP + ":" + pa.Backend.ServicePort.String(), - Weight: 1, - } } } } diff --git a/provider/kubernetes_test.go b/provider/kubernetes_test.go index 4f504f668..c525344bf 100644 --- a/provider/kubernetes_test.go +++ b/provider/kubernetes_test.go @@ -1 +1,184 @@ package provider + +import ( + "github.com/containous/traefik/provider/k8s" + "github.com/containous/traefik/types" + "reflect" + "testing" +) + +func TestLoadIngresses(t *testing.T) { + ingresses := []k8s.Ingress{{ + Spec: k8s.IngressSpec{ + Rules: []k8s.IngressRule{ + { + Host: "foo", + IngressRuleValue: k8s.IngressRuleValue{ + HTTP: &k8s.HTTPIngressRuleValue{ + Paths: []k8s.HTTPIngressPath{ + { + Path: "/bar", + Backend: k8s.IngressBackend{ + ServiceName: "service1", + ServicePort: k8s.FromInt(801), + }, + }, + }, + }, + }, + }, + { + Host: "bar", + IngressRuleValue: k8s.IngressRuleValue{ + HTTP: &k8s.HTTPIngressRuleValue{ + Paths: []k8s.HTTPIngressPath{ + { + Backend: k8s.IngressBackend{ + ServiceName: "service3", + ServicePort: k8s.FromInt(803), + }, + }, + { + Backend: k8s.IngressBackend{ + ServiceName: "service2", + ServicePort: k8s.FromInt(802), + }, + }, + }, + }, + }, + }, + }, + }, + }} + services := []k8s.Service{ + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service1", + UID: "1", + }, + Spec: k8s.ServiceSpec{ + ClusterIP: "10.0.0.1", + Ports: []k8s.ServicePort{ + { + Name: "http", + Port: 801, + }, + }, + }, + }, + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service2", + UID: "2", + }, + Spec: k8s.ServiceSpec{ + ClusterIP: "10.0.0.2", + Ports: []k8s.ServicePort{ + { + Name: "http", + Port: 802, + }, + }, + }, + }, + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service3", + UID: "3", + }, + Spec: k8s.ServiceSpec{ + ClusterIP: "10.0.0.3", + Ports: []k8s.ServicePort{ + { + Name: "http", + Port: 803, + }, + }, + }, + }, + } + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + watchChan: watchChan, + } + provider := Kubernetes{} + actual, err := provider.loadIngresses(client) + if err != nil { + t.Fatalf("error %+v", err) + } + + expected := &types.Configuration{ + Backends: map[string]*types.Backend{ + "foo/bar": { + Servers: map[string]types.Server{ + "1": { + URL: "http://10.0.0.1:801", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + "bar": { + Servers: map[string]types.Server{ + "2": { + URL: "http://10.0.0.2:802", + Weight: 1, + }, + "3": { + URL: "http://10.0.0.3:803", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + }, + Frontends: map[string]*types.Frontend{ + "foo/bar": { + Backend: "foo/bar", + Routes: map[string]types.Route{ + "/bar": { + Rule: "PathStrip:/bar", + }, + "foo": { + Rule: "Host:foo", + }, + }, + }, + "bar": { + Backend: "bar", + Routes: map[string]types.Route{ + "bar": { + Rule: "Host:bar", + }, + }, + }, + }, + } + 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) + } +} + +type clientMock struct { + ingresses []k8s.Ingress + services []k8s.Service + watchChan chan interface{} +} + +func (c clientMock) GetIngresses(predicate func(k8s.Ingress) bool) ([]k8s.Ingress, error) { + return c.ingresses, nil +} +func (c clientMock) WatchIngresses(predicate func(k8s.Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) { + return c.watchChan, make(chan error), nil +} +func (c clientMock) GetServices(predicate func(k8s.Service) bool) ([]k8s.Service, error) { + return c.services, nil +} diff --git a/templates/kubernetes.tmpl b/templates/kubernetes.tmpl index 01a21c73c..1f7dfaba1 100644 --- a/templates/kubernetes.tmpl +++ b/templates/kubernetes.tmpl @@ -11,6 +11,6 @@ backend = "{{$frontend.Backend}}" {{range $routeName, $route := $frontend.Routes}} [frontends."{{$frontendName}}".routes."{{$routeName}}"] - rule = "PathStrip:{{$route.Rule}}" + rule = "{{$route.Rule}}" {{end}} {{end}} diff --git a/webui/src/index.html b/webui/src/index.html index 20d356187..0edd91a06 100644 --- a/webui/src/index.html +++ b/webui/src/index.html @@ -40,10 +40,10 @@ From 9e14619a0b0c1e8ba483bffcddc8a5dd0df0aaea Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Wed, 20 Apr 2016 13:43:37 +0200 Subject: [PATCH 07/15] Add doc Signed-off-by: Emile Vauge --- docs/toml.md | 31 +++++++++++++++++++++++++++++++ examples/k8s.rc.yaml | 2 +- traefik.sample.toml | 20 ++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/docs/toml.md b/docs/toml.md index 5d929ccd6..65867d88f 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -614,6 +614,37 @@ Labels can be used on containers to override default behaviour: - `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. * `traefik.domain=traefik.localhost`: override the default domain + +## Kubernetes Ingress backend + + +Træfɪk can be configured to use Kubernetes Ingress as a backend configuration: + +```toml +################################################################ +# Kubernetes Ingress configuration backend +################################################################ +# Enable Kubernetes Ingress configuration backend +# +# Optional +# +[kubernetes] + +# Kubernetes server endpoint +# +# When deployed as a replication controller in Kubernetes, +# Traefik will use env variable KUBERNETES_SERVICE_HOST +# and KUBERNETES_SERVICE_PORT_HTTPS as endpoint +# Secure token will be found in /var/run/secrets/kubernetes.io/serviceaccount/token +# and SSL CA cert in /var/run/secrets/kubernetes.io/serviceaccount/ca.crt +# +# Optional +# +# endpoint = "http://localhost:8080" +``` + +You can find here an example [ingress](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s.ingress.yaml) and [replication controller](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s.rc.yaml). + ## Consul backend Træfɪk can be configured to use Consul as a backend configuration: diff --git a/examples/k8s.rc.yaml b/examples/k8s.rc.yaml index 74eb8aa62..9e1c85241 100644 --- a/examples/k8s.rc.yaml +++ b/examples/k8s.rc.yaml @@ -16,7 +16,7 @@ spec: spec: terminationGracePeriodSeconds: 60 containers: - - image: containous/traefik:k8s + - image: containous/traefik name: traefik-ingress-lb imagePullPolicy: Always ports: diff --git a/traefik.sample.toml b/traefik.sample.toml index 6141d1ea0..4275479e1 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -323,6 +323,26 @@ # [marathon.TLS] # InsecureSkipVerify = true +################################################################ +# Kubernetes Ingress configuration backend +################################################################ +# Enable Kubernetes Ingress configuration backend +# +# Optional +# +# [kubernetes] + +# Kubernetes server endpoint +# +# When deployed as a replication controller in Kubernetes, +# Traefik will use env variable KUBERNETES_SERVICE_HOST +# and KUBERNETES_SERVICE_PORT_HTTPS as endpoint +# Secure token will be found in /var/run/secrets/kubernetes.io/serviceaccount/token +# and SSL CA cert in /var/run/secrets/kubernetes.io/serviceaccount/ca.crt +# +# Optional +# +# endpoint = "http://localhost:8080" ################################################################ # Consul KV configuration backend From cac99273954cd76d5268a63271e1370326ce3b10 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Fri, 22 Apr 2016 11:33:41 +0200 Subject: [PATCH 08/15] Fix namespace, fix PathPrefixStrip Signed-off-by: Emile Vauge --- provider/k8s/client.go | 7 +++---- provider/kubernetes.go | 4 ++-- provider/kubernetes_test.go | 4 ++-- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/provider/k8s/client.go b/provider/k8s/client.go index 20e501cbb..770b9f788 100644 --- a/provider/k8s/client.go +++ b/provider/k8s/client.go @@ -15,7 +15,6 @@ import ( const ( // APIEndpoint defines the base path for kubernetes API resources. APIEndpoint = "/api/v1" - defaultService = "/namespaces/default/services" extentionsEndpoint = "/apis/extensions/v1beta1" defaultIngress = "/ingresses" ) @@ -24,7 +23,7 @@ const ( type Client interface { GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) WatchIngresses(predicate func(Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) - GetServices(predicate func(Service) bool) ([]Service, error) + GetServices(namespace string, predicate func(Service) bool) ([]Service, error) } type clientImpl struct { @@ -136,8 +135,8 @@ func (c *clientImpl) WatchIngresses(predicate func(Ingress) bool, stopCh <-chan } // GetServices returns all services in the cluster -func (c *clientImpl) GetServices(predicate func(Service) bool) ([]Service, error) { - getURL := c.endpointURL + APIEndpoint + defaultService +func (c *clientImpl) GetServices(namespace string, predicate func(Service) bool) ([]Service, error) { + getURL := c.endpointURL + APIEndpoint + "/namespaces/" + namespace + "/services" // Make request to Kubernetes API request := gorequest.New().Get(getURL) diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 0ea5f06bb..53cc07a16 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -136,10 +136,10 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur } if len(pa.Path) > 0 { templateObjects.Frontends[r.Host+pa.Path].Routes[pa.Path] = types.Route{ - Rule: "PathStrip:" + pa.Path, + Rule: "PathPrefixStrip:" + pa.Path, } } - services, err := k8sClient.GetServices(func(service k8s.Service) bool { + services, err := k8sClient.GetServices(i.Namespace, func(service k8s.Service) bool { return service.Name == pa.Backend.ServiceName }) if err != nil { diff --git a/provider/kubernetes_test.go b/provider/kubernetes_test.go index c525344bf..1b3dd1bd6 100644 --- a/provider/kubernetes_test.go +++ b/provider/kubernetes_test.go @@ -142,7 +142,7 @@ func TestLoadIngresses(t *testing.T) { Backend: "foo/bar", Routes: map[string]types.Route{ "/bar": { - Rule: "PathStrip:/bar", + Rule: "PathPrefixStrip:/bar", }, "foo": { Rule: "Host:foo", @@ -179,6 +179,6 @@ func (c clientMock) GetIngresses(predicate func(k8s.Ingress) bool) ([]k8s.Ingres func (c clientMock) WatchIngresses(predicate func(k8s.Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) { return c.watchChan, make(chan error), nil } -func (c clientMock) GetServices(predicate func(k8s.Service) bool) ([]k8s.Service, error) { +func (c clientMock) GetServices(namespace string, predicate func(k8s.Service) bool) ([]k8s.Service, error) { return c.services, nil } From 53a27876267e02cd613fc48598ea8aa35a921567 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Mon, 25 Apr 2016 16:56:06 +0200 Subject: [PATCH 09/15] Fix watch pods/services/rc/ingresses Signed-off-by: Emile Vauge --- examples/k8s.ingress.yaml | 2 +- examples/k8s.rc.yaml | 2 + provider/k8s/client.go | 238 +++++++++++++++++++++++++++--------- provider/kubernetes.go | 71 +++++++---- provider/kubernetes_test.go | 5 +- 5 files changed, 231 insertions(+), 87 deletions(-) diff --git a/examples/k8s.ingress.yaml b/examples/k8s.ingress.yaml index 0e460f48f..5b6c4c0d0 100644 --- a/examples/k8s.ingress.yaml +++ b/examples/k8s.ingress.yaml @@ -72,7 +72,7 @@ spec: apiVersion: extensions/v1beta1 kind: Ingress metadata: - name: whoamiIngress + name: whoami-ingress spec: rules: - host: foo.localhost diff --git a/examples/k8s.rc.yaml b/examples/k8s.rc.yaml index 9e1c85241..d7232b37d 100644 --- a/examples/k8s.rc.yaml +++ b/examples/k8s.rc.yaml @@ -24,6 +24,8 @@ spec: hostPort: 80 - containerPort: 443 hostPort: 443 + - containerPort: 8080 args: + - --web - --kubernetes - --logLevel=DEBUG \ No newline at end of file diff --git a/provider/k8s/client.go b/provider/k8s/client.go index 770b9f788..de26dd383 100644 --- a/provider/k8s/client.go +++ b/provider/k8s/client.go @@ -7,9 +7,11 @@ import ( "fmt" "github.com/containous/traefik/safe" "github.com/parnurzeal/gorequest" + "net" "net/http" "net/url" "strings" + "time" ) const ( @@ -22,8 +24,8 @@ const ( // Client is a client for the Kubernetes master. type Client interface { GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) - WatchIngresses(predicate func(Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) - GetServices(namespace string, predicate func(Service) bool) ([]Service, error) + GetServices(predicate func(Service) bool) ([]Service, error) + WatchAll(stopCh <-chan bool) (chan interface{}, chan error, error) } type clientImpl struct { @@ -52,19 +54,10 @@ func NewClient(baseURL string, caCert []byte, token string) (Client, error) { // GetIngresses returns all services in the cluster func (c *clientImpl) GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) { getURL := c.endpointURL + extentionsEndpoint + defaultIngress - request := gorequest.New().Get(getURL) - if len(c.token) > 0 { - request.Header["Authorization"] = "Bearer " + c.token - pool := x509.NewCertPool() - pool.AppendCertsFromPEM(c.caCert) - c.tls = &tls.Config{RootCAs: pool} - } - res, body, errs := request.TLSClientConfig(c.tls).EndBytes() - if errs != nil { - return nil, fmt.Errorf("failed to create request: GET %q : %v", getURL, errs) - } - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("http error %d GET %q: %q", res.StatusCode, getURL, string(body)) + + body, err := c.do(c.request(getURL)) + if err != nil { + return nil, fmt.Errorf("failed to create request: GET %q : %v", getURL, err) } var ingressList IngressList @@ -80,29 +73,186 @@ func (c *clientImpl) GetIngresses(predicate func(Ingress) bool) ([]Ingress, erro return ingresses, nil } -// WatchIngresses returns all services in the cluster -func (c *clientImpl) WatchIngresses(predicate func(Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) { +// WatchIngresses returns all ingresses in the cluster +func (c *clientImpl) WatchIngresses(stopCh <-chan bool) (chan interface{}, chan error, error) { + getURL := c.endpointURL + extentionsEndpoint + defaultIngress + return c.watch(getURL, stopCh) +} + +// GetServices returns all services in the cluster +func (c *clientImpl) GetServices(predicate func(Service) bool) ([]Service, error) { + getURL := c.endpointURL + APIEndpoint + "/services" + + body, err := c.do(c.request(getURL)) + if err != nil { + return nil, fmt.Errorf("failed to create request: GET %q : %v", getURL, err) + } + + var serviceList ServiceList + if err := json.Unmarshal(body, &serviceList); err != nil { + return nil, fmt.Errorf("failed to decode list of services resources: %v", err) + } + services := serviceList.Items[:0] + for _, service := range serviceList.Items { + if predicate(service) { + services = append(services, service) + } + } + return services, nil +} + +// WatchServices returns all services in the cluster +func (c *clientImpl) WatchServices(stopCh <-chan bool) (chan interface{}, chan error, error) { + getURL := c.endpointURL + APIEndpoint + "/services" + return c.watch(getURL, stopCh) +} + +// WatchEvents returns events in the cluster +func (c *clientImpl) WatchEvents(stopCh <-chan bool) (chan interface{}, chan error, error) { + getURL := c.endpointURL + APIEndpoint + "/events" + return c.watch(getURL, stopCh) +} + +// WatchPods returns pods in the cluster +func (c *clientImpl) WatchPods(stopCh <-chan bool) (chan interface{}, chan error, error) { + getURL := c.endpointURL + APIEndpoint + "/pods" + return c.watch(getURL, stopCh) +} + +// WatchReplicationControllers returns ReplicationControllers in the cluster +func (c *clientImpl) WatchReplicationControllers(stopCh <-chan bool) (chan interface{}, chan error, error) { + getURL := c.endpointURL + APIEndpoint + "/replicationcontrollers" + return c.watch(getURL, stopCh) +} + +// WatchAll returns events in the cluster +func (c *clientImpl) WatchAll(stopCh <-chan bool) (chan interface{}, chan error, error) { watchCh := make(chan interface{}) errCh := make(chan error) - getURL := c.endpointURL + extentionsEndpoint + defaultIngress + "?watch=true" + stopIngresses := make(chan bool) + chanIngresses, chanIngressesErr, err := c.WatchIngresses(stopIngresses) + if err != nil { + return watchCh, errCh, fmt.Errorf("failed to create watch %v", err) + } + stopServices := make(chan bool) + chanServices, chanServicesErr, err := c.WatchServices(stopServices) + if err != nil { + return watchCh, errCh, fmt.Errorf("failed to create watch %v", err) + } + stopPods := make(chan bool) + chanPods, chanPodsErr, err := c.WatchPods(stopPods) + if err != nil { + return watchCh, errCh, fmt.Errorf("failed to create watch %v", err) + } + stopReplicationControllers := make(chan bool) + chanReplicationControllers, chanReplicationControllersErr, err := c.WatchReplicationControllers(stopReplicationControllers) + if err != nil { + return watchCh, errCh, fmt.Errorf("failed to create watch %v", err) + } + go func() { + defer close(watchCh) + defer close(errCh) + defer close(stopIngresses) + defer close(stopServices) + defer close(stopPods) + defer close(stopReplicationControllers) + for { + select { + case <-stopCh: + stopIngresses <- true + stopServices <- true + stopPods <- true + stopReplicationControllers <- true + break + case err := <-chanIngressesErr: + errCh <- err + case err := <-chanServicesErr: + errCh <- err + case err := <-chanPodsErr: + errCh <- err + case err := <-chanReplicationControllersErr: + errCh <- err + case event := <-chanIngresses: + watchCh <- event + case event := <-chanServices: + watchCh <- event + case event := <-chanPods: + watchCh <- event + case event := <-chanReplicationControllers: + watchCh <- event + } + } + }() + + return watchCh, errCh, nil +} + +func (c *clientImpl) do(request *gorequest.SuperAgent) ([]byte, error) { + res, body, errs := request.EndBytes() + if errs != nil { + return nil, fmt.Errorf("failed to create request: GET %q : %v", request.Url, errs) + } + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("http error %d GET %q: %q", res.StatusCode, request.Url, string(body)) + } + return body, nil +} + +func (c *clientImpl) request(url string) *gorequest.SuperAgent { // Make request to Kubernetes API - request := gorequest.New().Get(getURL) + request := gorequest.New().Get(url) if len(c.token) > 0 { - request.Set("Authorization", "Bearer "+c.token) + request.Header["Authorization"] = "Bearer " + c.token pool := x509.NewCertPool() pool.AppendCertsFromPEM(c.caCert) c.tls = &tls.Config{RootCAs: pool} } + return request.TLSClientConfig(c.tls) +} + +// GenericObject generic object +type GenericObject struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty"` +} + +func (c *clientImpl) watch(url string, stopCh <-chan bool) (chan interface{}, chan error, error) { + watchCh := make(chan interface{}) + errCh := make(chan error) + + // get version + body, err := c.do(c.request(url)) + if err != nil { + return watchCh, errCh, fmt.Errorf("failed to create request: GET %q : %v", url, err) + } + + var generic GenericObject + if err := json.Unmarshal(body, &generic); err != nil { + return watchCh, errCh, fmt.Errorf("failed to create request: GET %q : %v", url, err) + } + resourceVersion := generic.ResourceVersion + + url = url + "?watch&resourceVersion=" + resourceVersion + // Make request to Kubernetes API + request := c.request(url) + request.Transport.Dial = func(network, addr string) (net.Conn, error) { + conn, err := net.Dial(network, addr) + if err != nil { + return nil, err + } + // No timeout for long-polling request + conn.SetDeadline(time.Now()) + return conn, nil + } req, err := request.TLSClientConfig(c.tls).MakeRequest() if err != nil { - return watchCh, errCh, fmt.Errorf("failed to create request: GET %q : %v", getURL, err) + return watchCh, errCh, fmt.Errorf("failed to create request: GET %q : %v", url, err) } - request.Client.Transport = request.Transport res, err := request.Client.Do(req) if err != nil { - return watchCh, errCh, fmt.Errorf("failed to make request: GET %q: %v", getURL, err) + return watchCh, errCh, fmt.Errorf("failed to make request: GET %q: %v", url, err) } shouldStop := safe.New(false) @@ -120,49 +270,15 @@ func (c *clientImpl) WatchIngresses(predicate func(Ingress) bool, stopCh <-chan defer close(watchCh) defer close(errCh) for { - var ingressList interface{} - if err := json.NewDecoder(res.Body).Decode(&ingressList); err != nil { + var eventList interface{} + if err := json.NewDecoder(res.Body).Decode(&eventList); err != nil { if !shouldStop.Get().(bool) { - errCh <- fmt.Errorf("failed to decode list of ingress resources: %v", err) + errCh <- fmt.Errorf("failed to decode watch event: %v", err) } return } - - watchCh <- ingressList + watchCh <- eventList } }() return watchCh, errCh, nil } - -// GetServices returns all services in the cluster -func (c *clientImpl) GetServices(namespace string, predicate func(Service) bool) ([]Service, error) { - getURL := c.endpointURL + APIEndpoint + "/namespaces/" + namespace + "/services" - - // Make request to Kubernetes API - request := gorequest.New().Get(getURL) - if len(c.token) > 0 { - request.Header["Authorization"] = "Bearer " + c.token - pool := x509.NewCertPool() - pool.AppendCertsFromPEM(c.caCert) - c.tls = &tls.Config{RootCAs: pool} - } - res, body, errs := request.TLSClientConfig(c.tls).EndBytes() - if errs != nil { - return nil, fmt.Errorf("failed to create request: GET %q : %v", getURL, errs) - } - if res.StatusCode != http.StatusOK { - return nil, fmt.Errorf("http error %d GET %q: %q", res.StatusCode, getURL, string(body)) - } - - var serviceList ServiceList - if err := json.Unmarshal(body, &serviceList); err != nil { - return nil, fmt.Errorf("failed to decode list of services resources: %v", err) - } - services := serviceList.Items[:0] - for _, service := range serviceList.Items { - if predicate(service) { - services = append(services, service) - } - } - return services, nil -} diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 53cc07a16..3fc484901 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -6,8 +6,10 @@ import ( "github.com/containous/traefik/provider/k8s" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" + "io" "io/ioutil" "os" + "strings" "text/template" "time" ) @@ -30,13 +32,13 @@ func (provider *Kubernetes) createClient() (k8s.Client, error) { token = string(tokenBytes) log.Debugf("Kubernetes token: %s", token) } else { - log.Debugf("Kubernetes load token error: %s", err) + log.Errorf("Kubernetes load token error: %s", err) } caCert, err := ioutil.ReadFile(serviceAccountCACert) if err == nil { log.Debugf("Kubernetes CA cert: %s", serviceAccountCACert) } else { - log.Debugf("Kubernetes load token error: %s", err) + log.Errorf("Kubernetes load token error: %s", err) } kubernetesHost := os.Getenv("KUBERNETES_SERVICE_HOST") kubernetesPort := os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS") @@ -54,38 +56,45 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage if err != nil { return err } + backOff := backoff.NewExponentialBackOff() pool.Go(func(stop chan bool) { stopWatch := make(chan bool) + defer close(stopWatch) operation := func() error { select { case <-stop: return nil default: } - ingressesChan, errChan, err := k8sClient.WatchIngresses(func(ingress k8s.Ingress) bool { - return true - }, stopWatch) - if err != nil { - log.Errorf("Error retrieving ingresses: %v", err) - return err - } for { - select { - case <-stop: - stopWatch <- true - return nil - case err := <-errChan: + eventsChan, errEventsChan, err := k8sClient.WatchAll(stopWatch) + if err != nil { + log.Errorf("Error watching kubernetes events: %v", err) return err - case event := <-ingressesChan: - log.Debugf("Received event from kubenetes %+v", event) - templateObjects, err := provider.loadIngresses(k8sClient) - if err != nil { + } + Watch: + for { + select { + case <-stop: + stopWatch <- true + return nil + case err := <-errEventsChan: + if strings.Contains(err.Error(), io.EOF.Error()) { + // edge case, kubernetes long-polling disconnection + break Watch + } return err - } - configurationChan <- types.ConfigMessage{ - ProviderName: "kubernetes", - Configuration: provider.loadConfig(*templateObjects), + case event := <-eventsChan: + log.Debugf("Received event from kubenetes %+v", event) + templateObjects, err := provider.loadIngresses(k8sClient) + if err != nil { + return err + } + configurationChan <- types.ConfigMessage{ + ProviderName: "kubernetes", + Configuration: provider.loadConfig(*templateObjects), + } } } } @@ -94,12 +103,21 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage notify := func(err error, time time.Duration) { log.Errorf("Kubernetes connection error %+v, retrying in %s", err, time) } - err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify) + err := backoff.RetryNotify(operation, backOff, notify) if err != nil { log.Fatalf("Cannot connect to Kubernetes server %+v", err) } }) + templateObjects, err := provider.loadIngresses(k8sClient) + if err != nil { + return err + } + configurationChan <- types.ConfigMessage{ + ProviderName: "kubernetes", + Configuration: provider.loadConfig(*templateObjects), + } + return nil } @@ -139,13 +157,18 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur Rule: "PathPrefixStrip:" + pa.Path, } } - services, err := k8sClient.GetServices(i.Namespace, func(service k8s.Service) bool { + services, err := k8sClient.GetServices(func(service k8s.Service) bool { return service.Name == pa.Backend.ServiceName }) if err != nil { log.Errorf("Error retrieving services: %v", err) continue } + if len(services) == 0 { + // no backends found, delete frontend... + delete(templateObjects.Frontends, r.Host+pa.Path) + log.Errorf("Error retrieving services %s", pa.Backend.ServiceName) + } for _, service := range services { var protocol string for _, port := range service.Spec.Ports { diff --git a/provider/kubernetes_test.go b/provider/kubernetes_test.go index 1b3dd1bd6..04d9b00b5 100644 --- a/provider/kubernetes_test.go +++ b/provider/kubernetes_test.go @@ -179,6 +179,9 @@ func (c clientMock) GetIngresses(predicate func(k8s.Ingress) bool) ([]k8s.Ingres func (c clientMock) WatchIngresses(predicate func(k8s.Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) { return c.watchChan, make(chan error), nil } -func (c clientMock) GetServices(namespace string, predicate func(k8s.Service) bool) ([]k8s.Service, error) { +func (c clientMock) GetServices(predicate func(k8s.Service) bool) ([]k8s.Service, error) { return c.services, nil } +func (c clientMock) WatchAll(stopCh <-chan bool) (chan interface{}, chan error, error) { + return c.watchChan, make(chan error), nil +} From 478eed6603774e6837ee3001b66280128a78db3b Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Tue, 26 Apr 2016 22:37:19 +0200 Subject: [PATCH 10/15] Updated libkv dependency In order to fix the TLS client authentication I have updated the libkv dependency. Now the connection to secured etcd and consuld should work properly. Signed-off-by: Thomas Boerger --- glide.lock | 2 +- glide.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/glide.lock b/glide.lock index a099df31a..735ee5d51 100644 --- a/glide.lock +++ b/glide.lock @@ -108,7 +108,7 @@ imports: - name: github.com/docker/libcompose version: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a - name: github.com/docker/libkv - version: 3732f7ff1b56057c3158f10bceb1e79133025373 + version: 7283ef27ed32fe267388510a91709b307bb9942c subpackages: - store - store/boltdb diff --git a/glide.yaml b/glide.yaml index 87905ad71..c922db6b9 100644 --- a/glide.yaml +++ b/glide.yaml @@ -33,7 +33,7 @@ import: - package: github.com/gorilla/handlers version: 40694b40f4a928c062f56849989d3e9cd0570e5f - package: github.com/docker/libkv - version: 3732f7ff1b56057c3158f10bceb1e79133025373 + version: 7283ef27ed32fe267388510a91709b307bb9942c - package: github.com/alecthomas/template version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 - package: github.com/vdemeester/shakers From b6b72c861fca71e07b8f5dafec16c3186450be38 Mon Sep 17 00:00:00 2001 From: Thomas Boerger Date: Tue, 26 Apr 2016 23:06:45 +0200 Subject: [PATCH 11/15] Replaced etcd-go with etcd/client In order to meet the requirements of the updated libkv library I have also replaced etcd-go with the up2date etcd client. Signed-off-by: Thomas Boerger --- glide.lock | 16 +++++++++++----- glide.yaml | 6 +++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/glide.lock b/glide.lock index 735ee5d51..a847744aa 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 6fe539ee86a9dc90a67b60f42b027c72359bed0ca22e7a94355ad80f37a32d68 -updated: 2016-04-18T21:31:13.195184921+02:00 +hash: e92948ce12f546d39a02c2e58668f7d12d7b1f3dd56eb046e01b527df756f734 +updated: 2016-04-26T23:18:15.861898862+02:00 imports: - name: github.com/alecthomas/template version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 @@ -31,10 +31,12 @@ imports: - utils - connlimit - stream -- name: github.com/coreos/go-etcd - version: cc90c7b091275e606ad0ca7102a23fb2072f3f5e +- name: github.com/coreos/etcd + version: 26e52d2bce9e3e11b77b68cc84bf91aebb1ef637 subpackages: - - etcd + - client + - pkg/pathutil + - pkg/types - name: github.com/davecgh/go-spew version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d subpackages: @@ -219,6 +221,10 @@ imports: - assert - name: github.com/thoas/stats version: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8 +- name: github.com/ugorji/go + version: ea9cd21fa0bc41ee4bdd50ac7ed8cbc7ea2ed960 + subpackages: + - codec - name: github.com/unrolled/render version: 26b4e3aac686940fe29521545afad9966ddfc80c - name: github.com/vdemeester/docker-events diff --git a/glide.yaml b/glide.yaml index c922db6b9..0115ec46f 100644 --- a/glide.yaml +++ b/glide.yaml @@ -1,9 +1,9 @@ package: main import: -- package: github.com/coreos/go-etcd - version: cc90c7b091275e606ad0ca7102a23fb2072f3f5e +- package: github.com/coreos/etcd + version: 26e52d2bce9e3e11b77b68cc84bf91aebb1ef637 subpackages: - - etcd + - client - package: github.com/mailgun/log version: 44874009257d4d47ba9806f1b7f72a32a015e4d8 - package: github.com/containous/oxy From 4ff4e4e62632c964398d6cb944020dff18be8bd8 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Tue, 26 Apr 2016 22:13:45 +0200 Subject: [PATCH 12/15] Fix Kubernetes watch SSL Signed-off-by: Emile Vauge --- provider/k8s/client.go | 43 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/provider/k8s/client.go b/provider/k8s/client.go index de26dd383..c576f9c17 100644 --- a/provider/k8s/client.go +++ b/provider/k8s/client.go @@ -7,11 +7,9 @@ import ( "fmt" "github.com/containous/traefik/safe" "github.com/parnurzeal/gorequest" - "net" "net/http" "net/url" "strings" - "time" ) const ( @@ -57,7 +55,7 @@ func (c *clientImpl) GetIngresses(predicate func(Ingress) bool) ([]Ingress, erro body, err := c.do(c.request(getURL)) if err != nil { - return nil, fmt.Errorf("failed to create request: GET %q : %v", getURL, err) + return nil, fmt.Errorf("failed to create ingresses request: GET %q : %v", getURL, err) } var ingressList IngressList @@ -85,7 +83,7 @@ func (c *clientImpl) GetServices(predicate func(Service) bool) ([]Service, error body, err := c.do(c.request(getURL)) if err != nil { - return nil, fmt.Errorf("failed to create request: GET %q : %v", getURL, err) + return nil, fmt.Errorf("failed to create services request: GET %q : %v", getURL, err) } var serviceList ServiceList @@ -133,22 +131,22 @@ func (c *clientImpl) WatchAll(stopCh <-chan bool) (chan interface{}, chan error, stopIngresses := make(chan bool) chanIngresses, chanIngressesErr, err := c.WatchIngresses(stopIngresses) if err != nil { - return watchCh, errCh, fmt.Errorf("failed to create watch %v", err) + return watchCh, errCh, fmt.Errorf("failed to create watch: %v", err) } stopServices := make(chan bool) chanServices, chanServicesErr, err := c.WatchServices(stopServices) if err != nil { - return watchCh, errCh, fmt.Errorf("failed to create watch %v", err) + return watchCh, errCh, fmt.Errorf("failed to create watch: %v", err) } stopPods := make(chan bool) chanPods, chanPodsErr, err := c.WatchPods(stopPods) if err != nil { - return watchCh, errCh, fmt.Errorf("failed to create watch %v", err) + return watchCh, errCh, fmt.Errorf("failed to create watch: %v", err) } stopReplicationControllers := make(chan bool) chanReplicationControllers, chanReplicationControllersErr, err := c.WatchReplicationControllers(stopReplicationControllers) if err != nil { - return watchCh, errCh, fmt.Errorf("failed to create watch %v", err) + return watchCh, errCh, fmt.Errorf("failed to create watch: %v", err) } go func() { defer close(watchCh) @@ -225,34 +223,35 @@ func (c *clientImpl) watch(url string, stopCh <-chan bool) (chan interface{}, ch // get version body, err := c.do(c.request(url)) if err != nil { - return watchCh, errCh, fmt.Errorf("failed to create request: GET %q : %v", url, err) + return watchCh, errCh, fmt.Errorf("failed to do version request: GET %q : %v", url, err) } var generic GenericObject if err := json.Unmarshal(body, &generic); err != nil { - return watchCh, errCh, fmt.Errorf("failed to create request: GET %q : %v", url, err) + return watchCh, errCh, fmt.Errorf("failed to decode version %v", err) } resourceVersion := generic.ResourceVersion url = url + "?watch&resourceVersion=" + resourceVersion // Make request to Kubernetes API request := c.request(url) - request.Transport.Dial = func(network, addr string) (net.Conn, error) { - conn, err := net.Dial(network, addr) - if err != nil { - return nil, err - } - // No timeout for long-polling request - conn.SetDeadline(time.Now()) - return conn, nil - } - req, err := request.TLSClientConfig(c.tls).MakeRequest() + // request.Transport.Dial = func(network, addr string) (net.Conn, error) { + // conn, err := net.Dial(network, addr) + // if err != nil { + // return nil, err + // } + // // No timeout for long-polling request + // conn.SetDeadline(time.Now()) + // return conn, nil + // } + req, err := request.MakeRequest() if err != nil { - return watchCh, errCh, fmt.Errorf("failed to create request: GET %q : %v", url, err) + return watchCh, errCh, fmt.Errorf("failed to make watch request: GET %q : %v", url, err) } + request.Client.Transport = request.Transport res, err := request.Client.Do(req) if err != nil { - return watchCh, errCh, fmt.Errorf("failed to make request: GET %q: %v", url, err) + return watchCh, errCh, fmt.Errorf("failed to do watch request: GET %q: %v", url, err) } shouldStop := safe.New(false) From 87caf458df5b98d36c35d713fae0be56e068f3d2 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Tue, 26 Apr 2016 22:26:25 +0200 Subject: [PATCH 13/15] Fix Kubernetes schema Signed-off-by: Emile Vauge --- examples/k8s.ingress.yaml | 2 +- provider/k8s/client.go | 9 --------- provider/kubernetes.go | 6 ++++-- provider/kubernetes_test.go | 18 +++++++++--------- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/examples/k8s.ingress.yaml b/examples/k8s.ingress.yaml index 5b6c4c0d0..aac7798e4 100644 --- a/examples/k8s.ingress.yaml +++ b/examples/k8s.ingress.yaml @@ -12,7 +12,7 @@ spec: nodePort: 30283 targetPort: 80 protocol: TCP - name: http + name: https selector: app: whoami --- diff --git a/provider/k8s/client.go b/provider/k8s/client.go index c576f9c17..b03bb0e7d 100644 --- a/provider/k8s/client.go +++ b/provider/k8s/client.go @@ -235,15 +235,6 @@ func (c *clientImpl) watch(url string, stopCh <-chan bool) (chan interface{}, ch url = url + "?watch&resourceVersion=" + resourceVersion // Make request to Kubernetes API request := c.request(url) - // request.Transport.Dial = func(network, addr string) (net.Conn, error) { - // conn, err := net.Dial(network, addr) - // if err != nil { - // return nil, err - // } - // // No timeout for long-polling request - // conn.SetDeadline(time.Now()) - // return conn, nil - // } req, err := request.MakeRequest() if err != nil { return watchCh, errCh, fmt.Errorf("failed to make watch request: GET %q : %v", url, err) diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 3fc484901..dc7b395ff 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -170,10 +170,12 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur log.Errorf("Error retrieving services %s", pa.Backend.ServiceName) } for _, service := range services { - var protocol string + protocol := "http" for _, port := range service.Spec.Ports { if port.Port == pa.Backend.ServicePort.IntValue() { - protocol = port.Name + if port.Port == 443 { + protocol = "https" + } templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{ URL: protocol + "://" + service.Spec.ClusterIP + ":" + pa.Backend.ServicePort.String(), Weight: 1, diff --git a/provider/kubernetes_test.go b/provider/kubernetes_test.go index 04d9b00b5..13daf6e58 100644 --- a/provider/kubernetes_test.go +++ b/provider/kubernetes_test.go @@ -1,6 +1,7 @@ package provider import ( + "encoding/json" "github.com/containous/traefik/provider/k8s" "github.com/containous/traefik/types" "reflect" @@ -35,7 +36,7 @@ func TestLoadIngresses(t *testing.T) { { Backend: k8s.IngressBackend{ ServiceName: "service3", - ServicePort: k8s.FromInt(803), + ServicePort: k8s.FromInt(443), }, }, { @@ -76,7 +77,6 @@ func TestLoadIngresses(t *testing.T) { ClusterIP: "10.0.0.2", Ports: []k8s.ServicePort{ { - Name: "http", Port: 802, }, }, @@ -92,7 +92,7 @@ func TestLoadIngresses(t *testing.T) { Ports: []k8s.ServicePort{ { Name: "http", - Port: 803, + Port: 443, }, }, }, @@ -129,7 +129,7 @@ func TestLoadIngresses(t *testing.T) { Weight: 1, }, "3": { - URL: "http://10.0.0.3:803", + URL: "https://10.0.0.3:443", Weight: 1, }, }, @@ -159,11 +159,11 @@ func TestLoadIngresses(t *testing.T) { }, }, } - 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) + actualJSON, _ := json.Marshal(actual) + expectedJSON, _ := json.Marshal(expected) + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("expected %+v, got %+v", string(expectedJSON), string(actualJSON)) } } From 10cb60657813af2cd802de392f7f4434cb0eae22 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Tue, 26 Apr 2016 23:54:00 +0200 Subject: [PATCH 14/15] Add Kubernetes URL Signed-off-by: Emile Vauge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f27915984..de664d1c7 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Træfɪk is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease. -It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), Rest API, file...) to manage its configuration automatically and dynamically. +It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Kubernetes](http://kubernetes.io/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), Rest API, file...) to manage its configuration automatically and dynamically. ## Overview From 38371234a22cdd9a50eb2a8e5911215d1791c320 Mon Sep 17 00:00:00 2001 From: Emile Vauge Date: Wed, 27 Apr 2016 00:02:03 +0200 Subject: [PATCH 15/15] Add logo credits Signed-off-by: Emile Vauge --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index de664d1c7..21a4ffd9f 100644 --- a/README.md +++ b/README.md @@ -133,8 +133,11 @@ Europe. We provide consulting, development, training and support for the world software products. - [![Asteris](docs/img/asteris.logo.png)](https://aster.is) Founded in 2014, Asteris creates next-generation infrastructure software for the modern datacenter. Asteris writes software that makes it easy for companies to implement continuous delivery and realtime data pipelines. We support the HashiCorp stack, along with Kubernetes, Apache Mesos, Spark and Kafka. We're core committers on mantl.io, consul-cli and mesos-consul. . + +## Credits + +Thanks you [Peka](http://peka.byethost11.com/photoblog/) for your awesome work on the logo ![logo](docs/img/traefik.icon.png) \ No newline at end of file