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/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/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..5b6c4c0d0 --- /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: service1 + labels: + app: whoami +spec: + type: NodePort + ports: + - port: 80 + nodePort: 30283 + targetPort: 80 + protocol: TCP + name: http + selector: + app: whoami +--- +apiVersion: v1 +kind: Service +metadata: + name: service2 + labels: + app: whoami +spec: + type: NodePort + ports: + - port: 80 + nodePort: 30284 + targetPort: 80 + protocol: TCP + name: http + 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 +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: whoami-ingress +spec: + rules: + - host: foo.localhost + http: + paths: + - path: /bar + backend: + serviceName: service1 + servicePort: 80 + - host: bar.localhost + http: + paths: + - backend: + serviceName: service2 + servicePort: 80 + - backend: + serviceName: service3 + 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..d7232b37d --- /dev/null +++ b/examples/k8s.rc.yaml @@ -0,0 +1,31 @@ +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 + name: traefik-ingress-lb + imagePullPolicy: Always + ports: + - containerPort: 80 + hostPort: 80 + - containerPort: 443 + hostPort: 443 + - containerPort: 8080 + args: + - --web + - --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..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,25 +17,45 @@ type SimpleSuite struct{ BaseSuite } func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) { cmd := exec.Command(traefikBinary) - output, err := cmd.CombinedOutput() - c.Assert(err, checker.NotNil) - c.Assert(string(output), checker.Contains, "Error reading file: open : no such file or directory") + 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) - c.Assert(string(output), checker.Contains, fmt.Sprintf("Error reading file: open %s: no such file or directory", nonExistentFile)) + 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) - c.Assert(string(output), checker.Contains, "Error reading file: While parsing config: Near line 1") + 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 '{'") } 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..de26dd383 --- /dev/null +++ b/provider/k8s/client.go @@ -0,0 +1,284 @@ +package k8s + +import ( + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "github.com/containous/traefik/safe" + "github.com/parnurzeal/gorequest" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +const ( + // APIEndpoint defines the base path for kubernetes API resources. + APIEndpoint = "/api/v1" + extentionsEndpoint = "/apis/extensions/v1beta1" + defaultIngress = "/ingresses" +) + +// Client is a client for the Kubernetes master. +type Client interface { + GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) + GetServices(predicate func(Service) bool) ([]Service, error) + WatchAll(stopCh <-chan bool) (chan interface{}, chan error, error) +} + +type clientImpl 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 &clientImpl{ + endpointURL: strings.TrimSuffix(validURL.String(), "/"), + token: token, + caCert: caCert, + }, nil +} + +// GetIngresses returns all services in the cluster +func (c *clientImpl) GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) { + getURL := c.endpointURL + extentionsEndpoint + defaultIngress + + 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 + 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 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) + + 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(url) + if len(c.token) > 0 { + 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", url, err) + } + res, err := request.Client.Do(req) + if err != nil { + return watchCh, errCh, fmt.Errorf("failed to make request: GET %q: %v", url, 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 eventList interface{} + if err := json.NewDecoder(res.Body).Decode(&eventList); err != nil { + if !shouldStop.Get().(bool) { + errCh <- fmt.Errorf("failed to decode watch event: %v", err) + } + return + } + watchCh <- eventList + } + }() + return watchCh, errCh, 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..e501718ce --- /dev/null +++ b/provider/k8s/service.go @@ -0,0 +1,326 @@ +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 +} + +// 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 { + 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..3fc484901 --- /dev/null +++ b/provider/kubernetes.go @@ -0,0 +1,198 @@ +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" + "io/ioutil" + "os" + "strings" + "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 +} + +func (provider *Kubernetes) createClient() (k8s.Client, error) { + var token string + tokenBytes, err := ioutil.ReadFile(serviceAccountToken) + if err == nil { + token = string(tokenBytes) + log.Debugf("Kubernetes token: %s", token) + } else { + 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.Errorf("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) + 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 + } + 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: + } + for { + eventsChan, errEventsChan, err := k8sClient.WatchAll(stopWatch) + if err != nil { + log.Errorf("Error watching kubernetes events: %v", err) + return err + } + 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 + 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), + } + } + } + } + } + + notify := func(err error, time time.Duration) { + log.Errorf("Kubernetes connection error %+v, retrying in %s", err, time) + } + 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 +} + +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: "PathPrefixStrip:" + 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 + } + 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 { + 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 + } + } + } + } + } + } + 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) + if err != nil { + log.Error(err) + } + return configuration +} diff --git a/provider/kubernetes_test.go b/provider/kubernetes_test.go new file mode 100644 index 000000000..04d9b00b5 --- /dev/null +++ b/provider/kubernetes_test.go @@ -0,0 +1,187 @@ +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: "PathPrefixStrip:/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 +} +func (c clientMock) WatchAll(stopCh <-chan bool) (chan interface{}, chan error, error) { + return c.watchChan, make(chan error), nil +} 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/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}} 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}} 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 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 @@