Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Marcus Stong 2016-05-02 11:44:07 -04:00
commit 52bff85dda
50 changed files with 2677 additions and 291 deletions

2
.gitignore vendored
View file

@ -11,3 +11,5 @@ vendor/
static/ static/
.vscode/ .vscode/
site/ site/
*.log
*.exe

View file

@ -13,7 +13,7 @@
Træfɪk is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease. 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 ## Overview
@ -76,7 +76,7 @@ You can access to a simple HTML frontend of Træfik.
## Plumbing ## 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 - [Gorilla mux](https://github.com/gorilla/mux): famous request router
- [Negroni](https://github.com/codegangsta/negroni): web middlewares made simple - [Negroni](https://github.com/codegangsta/negroni): web middlewares made simple
- [Manners](https://github.com/mailgun/manners): graceful shutdown of http.Handler servers - [Manners](https://github.com/mailgun/manners): graceful shutdown of http.Handler servers
@ -133,8 +133,11 @@ Europe. We provide consulting, development, training and support for the world
software products. software products.
[![Asteris](docs/img/asteris.logo.png)](https://aster.is) [![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. 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)

View file

@ -181,7 +181,7 @@ func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(doma
acme.Logger = fmtlog.New(ioutil.Discard, "", 0) acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
if len(a.StorageFile) == 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...") log.Debugf("Generating default certificate...")

8
cmd.go
View file

@ -51,6 +51,7 @@ var arguments = struct {
etcd bool etcd bool
etcdTLS bool etcdTLS bool
boltdb bool boltdb bool
kubernetes bool
}{ }{
GlobalConfiguration{ GlobalConfiguration{
EntryPoints: make(EntryPoints), EntryPoints: make(EntryPoints),
@ -72,7 +73,8 @@ var arguments = struct {
TLS: &provider.KvTLS{}, TLS: &provider.KvTLS{},
}, },
}, },
Boltdb: &provider.BoltDb{}, Boltdb: &provider.BoltDb{},
Kubernetes: &provider.Kubernetes{},
}, },
false, false,
false, false,
@ -86,6 +88,7 @@ var arguments = struct {
false, false,
false, false,
false, false,
false,
} }
func init() { 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.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().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("configFile", traefikCmd.PersistentFlags().Lookup("configFile"))
_ = viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut")) _ = viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut"))
_ = viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel")) _ = viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel"))

View file

@ -37,6 +37,7 @@ type GlobalConfiguration struct {
Etcd *provider.Etcd Etcd *provider.Etcd
Zookeeper *provider.Zookepper Zookeeper *provider.Zookepper
Boltdb *provider.BoltDb Boltdb *provider.BoltDb
Kubernetes *provider.Kubernetes
} }
// DefaultEntryPoints holds default entry points // 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("$HOME/.traefik/") // call multiple times to add many search paths
viper.AddConfigPath(".") // optionally look for config in the working directory viper.AddConfigPath(".") // optionally look for config in the working directory
if err := viper.ReadInConfig(); err != nil { 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 { if len(arguments.EntryPoints) > 0 {
@ -254,6 +259,9 @@ func LoadConfiguration() *GlobalConfiguration {
if arguments.boltdb { if arguments.boltdb {
viper.Set("boltdb", arguments.Boltdb) viper.Set("boltdb", arguments.Boltdb)
} }
if arguments.kubernetes {
viper.Set("kubernetes", arguments.Kubernetes)
}
if err := unmarshal(&configuration); err != nil { if err := unmarshal(&configuration); err != nil {
fmtlog.Fatalf("Error reading file: %s", err) fmtlog.Fatalf("Error reading file: %s", err)

View file

@ -19,7 +19,7 @@ Let's zoom on Træfɪk and have an overview of its internal architecture:
![Architecture](img/internal.png) ![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...). - 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. 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. - 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. - Finally, the [server](#servers) will forward the request to the corresponding microservice in the private network.
@ -142,7 +142,7 @@ For example:
## Servers ## 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: Here is an example of backends and servers definition:

View file

@ -90,7 +90,7 @@
# regex = "^http://localhost/(.*)" # regex = "^http://localhost/(.*)"
# replacement = "http://mydomain/$1" # replacement = "http://mydomain/$1"
entryPoints] [entryPoints]
[entryPoints.http] [entryPoints.http]
address = ":80" address = ":80"
``` ```
@ -265,7 +265,7 @@ defaultEntryPoints = ["http", "https"]
rule = "Path:/test" 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 ```toml
# traefik.toml # traefik.toml
@ -621,6 +621,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.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
* `traefik.domain=traefik.localhost`: override the default domain * `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 ## Consul backend
Træfɪk can be configured to use Consul as a backend configuration: Træfɪk can be configured to use Consul as a backend configuration:

2
examples/accessLog/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
exampleHandler
exampleHandler.exe

View file

@ -0,0 +1,46 @@
/*
Simple program to start a web server on a specified port
*/
package main
import (
"flag"
"fmt"
"net/http"
"os"
)
var (
name string
port int
help *bool
)
func init() {
flag.StringVar(&name, "n", "", "Name of handler for messages")
flag.IntVar(&port, "p", 0, "Port number to listen")
help = flag.Bool("h", false, "Displays help message")
}
func usage() {
fmt.Printf("Usage: example -n name -p port \n")
os.Exit(2)
}
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s: Received query %s!\n", name, r.URL.Path[1:])
}
func main() {
flag.Parse()
if *help || len(name) == 0 || port <= 0 {
usage()
}
http.HandleFunc("/", handler)
fmt.Printf("%s: Listening on :%d...\n", name, port)
if er := http.ListenAndServe(fmt.Sprintf(":%d", port), nil); er != nil {
fmt.Printf("%s: Error from ListenAndServe: %s", name, er.Error())
os.Exit(1)
}
fmt.Printf("%s: How'd we get past listen and serve???\n", name)
}

122
examples/accessLog/runAb.sh Executable file
View file

@ -0,0 +1,122 @@
#!/bin/bash
usage()
{
echo 'runAb.sh - Run Apache Benchmark to test access log'
echo ' Usage: runAb.sh [--conn nnn] [--log xxx] [--num nnn] [--time nnn] [--wait nn]'
echo ' -c|--conn - number of simultaneous connections (default 100)'
echo ' -l|--log - name of logfile (default benchmark.log)'
echo ' -n|--num - number of requests (default 50000); ignored when -t specified'
echo ' -t|--time - time in seconds for benchmark (default no limit)'
echo ' -w|--wait - number of seconds to wait for Traefik to initialize (default 15)'
echo ' '
exit
}
# Parse options
conn=100
num=50000
wait=15
time=0
logfile=""
while [[ $1 =~ ^- ]]
do
case $1 in
-c|--conn)
conn=$2
shift
;;
-h|--help)
usage
;;
-l|--log|--logfile)
logfile=$2
shift
;;
-n|--num)
num=$2
shift
;;
-t|--time)
time=$2
shift
;;
-w|--wait)
wait=$2
shift
;;
*)
echo Unknown option "$1"
usage
esac
shift
done
if [ -z "$logfile" ] ; then
logfile="benchmark.log"
fi
# Change to accessLog examples directory
[ -d examples/accessLog ] && cd examples/accessLog
if [ ! -r exampleHandler.go ] ; then
echo Please run this script either from the traefik repo root or from the examples/accessLog directory
exit
fi
# Kill traefik and any running example processes
sudo pkill -f traefik
pkill -f exampleHandler
[ ! -d log ] && mkdir log
# Start new example processes
go build exampleHandler.go
[ $? -ne 0 ] && exit $?
./exampleHandler -n Handler1 -p 8081 &
[ $? -ne 0 ] && exit $?
./exampleHandler -n Handler2 -p 8082 &
[ $? -ne 0 ] && exit $?
./exampleHandler -n Handler3 -p 8083 &
[ $? -ne 0 ] && exit $?
# Wait a couple of seconds for handlers to initialize and start Traefik
cd ../..
sleep 2s
echo Starting Traefik...
sudo ./traefik -c examples/accessLog/traefik.ab.toml &
[ $? -ne 0 ] && exit $?
# Wait for Traefik to initialize and run ab
echo Waiting $wait seconds before starting ab benchmark
sleep ${wait}s
echo
stime=`date '+%s'`
if [ $time -eq 0 ] ; then
echo Benchmark starting `date` with $conn connections until $num requests processed | tee $logfile
echo | tee -a $logfile
echo ab -k -c $conn -n $num http://127.0.0.1/test | tee -a $logfile
echo | tee -a $logfile
ab -k -c $conn -n $num http://127.0.0.1/test 2>&1 | tee -a $logfile
else
if [ $num -ne 50000 ] ; then
echo Request count ignored when --time specified
fi
echo Benchmark starting `date` with $conn connections for $time seconds | tee $logfile
echo | tee -a $logfile
echo ab -k -c $conn -t $time -n 100000000 http://127.0.0.1/test | tee -a $logfile
echo | tee -a $logfile
ab -k -c $conn -t $time -n 100000000 http://127.0.0.1/test 2>&1 | tee -a $logfile
fi
etime=`date '+%s'`
let "dt=$etime - $stime"
let "ds=$dt % 60"
let "dm=($dt / 60) % 60"
let "dh=$dt / 3600"
echo | tee -a $logfile
printf "Benchmark ended `date` after %d:%02d:%02d\n" $dh $dm $ds | tee -a $logfile
echo Results available in $logfile

View file

@ -0,0 +1,40 @@
#!/bin/bash
# Script to run a three-server example. This script runs the three servers and restarts Traefik
# Once it is running, use the command:
#
# curl http://127.0.0.1:80/test{1,2,2}
#
# to send requests to send test requests to the servers. You should see a response like:
#
# Handler1: received query test1!
# Handler2: received query test2!
# Handler3: received query test2!
#
# and can then inspect log/access.log to see frontend, backend, and timing
# Kill traefik and any running example processes
sudo pkill -f traefik
pkill -f exampleHandler
[ ! -d log ] && mkdir log
# Start new example processes
cd examples/accessLog
go build exampleHandler.go
[ $? -ne 0 ] && exit $?
./exampleHandler -n Handler1 -p 8081 &
[ $? -ne 0 ] && exit $?
./exampleHandler -n Handler2 -p 8082 &
[ $? -ne 0 ] && exit $?
./exampleHandler -n Handler3 -p 8083 &
[ $? -ne 0 ] && exit $?
# Wait a couple of seconds for handlers to initialize and start Traefik
cd ../..
sleep 2s
echo Starting Traefik...
sudo ./traefik -c examples/accessLog/traefik.example.toml &
[ $? -ne 0 ] && exit $?
echo Sample handlers and traefik started successfully!
echo 'Use command curl http://127.0.0.1:80/test{1,2,2} to drive test'
echo Then inspect log/access.log to verify it contains frontend, backend, and timing

View file

@ -0,0 +1,37 @@
################################################################
# Global configuration
################################################################
traefikLogsFile = "log/traefik.log"
accessLogsFile = "log/access.log"
logLevel = "DEBUG"
################################################################
# Web configuration backend
################################################################
[web]
address = ":7888"
################################################################
# File configuration backend
################################################################
[file]
################################################################
# rules
################################################################
[backends]
[backends.backend]
[backends.backend.LoadBalancer]
method = "drr"
[backends.backend.servers.server1]
url = "http://127.0.0.1:8081"
[backends.backend.servers.server2]
url = "http://127.0.0.1:8082"
[backends.backend.servers.server3]
url = "http://127.0.0.1:8083"
[frontends]
[frontends.frontend]
backend = "backend"
passHostHeader = true
[frontends.frontend.routes.test]
rule = "Path: /test"

View file

@ -0,0 +1,42 @@
################################################################
# Global configuration
################################################################
traefikLogsFile = "log/traefik.log"
accessLogsFile = "log/access.log"
logLevel = "DEBUG"
################################################################
# Web configuration backend
################################################################
[web]
address = ":7888"
################################################################
# File configuration backend
################################################################
[file]
################################################################
# rules
################################################################
[backends]
[backends.backend1]
[backends.backend1.servers.server1]
url = "http://127.0.0.1:8081"
[backends.backend2]
[backends.backend2.LoadBalancer]
method = "drr"
[backends.backend2.servers.server1]
url = "http://127.0.0.1:8082"
[backends.backend2.servers.server2]
url = "http://127.0.0.1:8083"
[frontends]
[frontends.frontend1]
backend = "backend1"
[frontends.frontend1.routes.test_1]
rule = "Path: /test1"
[frontends.frontend2]
backend = "backend2"
passHostHeader = true
[frontends.frontend2.routes.test_2]
rule = "Path: /test2"

17
examples/compose-k8s.yaml Normal file
View file

@ -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']

View file

@ -43,7 +43,7 @@ marathon:
command: --event_subscriber http_callback command: --event_subscriber http_callback
traefik: traefik:
image: traefik image: containous/traefik
command: -c /dev/null --web --logLevel=DEBUG --marathon --marathon.domain marathon.localhost --marathon.endpoint http://172.17.0.1:8080 --marathon.watch command: -c /dev/null --web --logLevel=DEBUG --marathon --marathon.domain marathon.localhost --marathon.endpoint http://172.17.0.1:8080 --marathon.watch
ports: ports:
- "8000:80" - "8000:80"

View file

@ -17,11 +17,9 @@ curl -i -H "Accept: application/json" -X PUT -d "2" ht
# frontend 1 # 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 "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 "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 "Host:test.localhost" 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
# frontend 2 # 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 "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 "http" 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 "Path:/test" 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

93
examples/k8s.ingress.yaml Normal file
View file

@ -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: https
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

10
examples/k8s.namespace.sh Executable file
View file

@ -0,0 +1,10 @@
#!/bin/bash
kubectl create -f - << EOF
kind: Namespace
apiVersion: v1
metadata:
name: kube-system
labels:
name: kube-system
EOF

31
examples/k8s.rc.yaml Normal file
View file

@ -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

View file

@ -25,7 +25,7 @@
], ],
"labels": { "labels": {
"traefik.weight": "1", "traefik.weight": "1",
"traefik.protocole": "http", "traefik.protocol": "http",
"traefik.frontend.rule" : "Host:test.marathon.localhost" "traefik.frontend.rule" : "Host:test.marathon.localhost"
} }
} }

42
glide.lock generated
View file

@ -1,5 +1,5 @@
hash: fffa87220825895f7e3a6ceed3b13ecbf6bc934ab072fc9be3d00e3eef411ecb hash: a9f41b9fe89ac3028da27ac9cbe31db9a79ae89082f42507d4d0c58290517ee2
updated: 2016-04-13T14:05:41.300658168+02:00 updated: 2016-04-27T17:14:45.61228359Z
imports: imports:
- name: github.com/alecthomas/template - name: github.com/alecthomas/template
version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0
@ -8,7 +8,7 @@ imports:
- name: github.com/boltdb/bolt - name: github.com/boltdb/bolt
version: 51f99c862475898df9773747d3accd05a7ca33c1 version: 51f99c862475898df9773747d3accd05a7ca33c1
- name: github.com/BurntSushi/toml - name: github.com/BurntSushi/toml
version: bd2bdf7f18f849530ef7a1c29a4290217cab32a1 version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f
- name: github.com/BurntSushi/ty - name: github.com/BurntSushi/ty
version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74 version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74
subpackages: subpackages:
@ -29,11 +29,14 @@ imports:
- memmetrics - memmetrics
- roundrobin - roundrobin
- utils - utils
- connlimit
- stream - stream
- name: github.com/coreos/go-etcd - name: github.com/coreos/etcd
version: cc90c7b091275e606ad0ca7102a23fb2072f3f5e version: 26e52d2bce9e3e11b77b68cc84bf91aebb1ef637
subpackages: subpackages:
- etcd - client
- pkg/pathutil
- pkg/types
- name: github.com/davecgh/go-spew - name: github.com/davecgh/go-spew
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
subpackages: subpackages:
@ -42,7 +45,6 @@ imports:
version: ff6f38ccb69afa96214c7ee955359465d1fc767a version: ff6f38ccb69afa96214c7ee955359465d1fc767a
subpackages: subpackages:
- reference - reference
- digest
- name: github.com/docker/docker - name: github.com/docker/docker
version: f39987afe8d611407887b3094c03d6ba6a766a67 version: f39987afe8d611407887b3094c03d6ba6a766a67
subpackages: subpackages:
@ -94,10 +96,8 @@ imports:
- client/transport - client/transport
- client/transport/cancellable - client/transport/cancellable
- types/network - types/network
- types/reference
- types/registry - types/registry
- types/time - types/time
- types/versions
- types/blkiodev - types/blkiodev
- name: github.com/docker/go-connections - name: github.com/docker/go-connections
version: f549a9393d05688dff0992ef3efd8bbe6c628aeb version: f549a9393d05688dff0992ef3efd8bbe6c628aeb
@ -110,7 +110,7 @@ imports:
- name: github.com/docker/libcompose - name: github.com/docker/libcompose
version: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a version: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a
- name: github.com/docker/libkv - name: github.com/docker/libkv
version: 3732f7ff1b56057c3158f10bceb1e79133025373 version: 7283ef27ed32fe267388510a91709b307bb9942c
subpackages: subpackages:
- store - store
- store/boltdb - store/boltdb
@ -121,6 +121,12 @@ imports:
version: 9cbd2a1374f46905c68a4eb3694a130610adc62a version: 9cbd2a1374f46905c68a4eb3694a130610adc62a
- name: github.com/donovanhide/eventsource - name: github.com/donovanhide/eventsource
version: d8a3071799b98cacd30b6da92f536050ccfe6da4 version: d8a3071799b98cacd30b6da92f536050ccfe6da4
- name: github.com/eapache/go-resiliency
version: b86b1ec0dd4209a588dc1285cdd471e73525c0b3
subpackages:
- breaker
- name: github.com/eapache/queue
version: ded5959c0d4e360646dc9e9908cff48666781367
- name: github.com/elazarl/go-bindata-assetfs - name: github.com/elazarl/go-bindata-assetfs
version: d5cac425555ca5cf00694df246e04f05e6a55150 version: d5cac425555ca5cf00694df246e04f05e6a55150
- name: github.com/flynn/go-shlex - name: github.com/flynn/go-shlex
@ -131,6 +137,8 @@ imports:
version: 11d3bc7aa68e238947792f30573146a3231fc0f1 version: 11d3bc7aa68e238947792f30573146a3231fc0f1
- name: github.com/golang/glog - name: github.com/golang/glog
version: fca8c8854093a154ff1eb580aae10276ad6b1b5f version: fca8c8854093a154ff1eb580aae10276ad6b1b5f
- name: github.com/golang/snappy
version: ec642410cd033af63620b66a91ccbd3c69c2c59a
- name: github.com/google/go-querystring - name: github.com/google/go-querystring
version: 9235644dd9e52eeae6fa48efd539fdc351a0af53 version: 9235644dd9e52eeae6fa48efd539fdc351a0af53
subpackages: subpackages:
@ -160,6 +168,8 @@ imports:
- json/token - json/token
- name: github.com/inconshreveable/mousetrap - name: github.com/inconshreveable/mousetrap
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
- name: github.com/klauspost/crc32
version: 19b0b332c9e4516a6370a0456e6182c3b5036720
- name: github.com/kr/pretty - name: github.com/kr/pretty
version: add1dbc86daf0f983cd4a48ceb39deb95c729b67 version: add1dbc86daf0f983cd4a48ceb39deb95c729b67
- name: github.com/kr/text - name: github.com/kr/text
@ -174,16 +184,22 @@ imports:
version: 565402cd71fbd9c12aa7e295324ea357e970a61e version: 565402cd71fbd9c12aa7e295324ea357e970a61e
- name: github.com/mailgun/timetools - name: github.com/mailgun/timetools
version: fd192d755b00c968d312d23f521eb0cdc6f66bd0 version: fd192d755b00c968d312d23f521eb0cdc6f66bd0
- name: github.com/mattn/go-shellwords
version: 525bedee691b5a8df547cb5cf9f86b7fb1883e24
- name: github.com/Microsoft/go-winio - name: github.com/Microsoft/go-winio
version: 862b6557927a5c5c81e411c12aa6de7e566cbb7a version: 862b6557927a5c5c81e411c12aa6de7e566cbb7a
- name: github.com/miekg/dns - name: github.com/miekg/dns
version: dd83d5cbcfd986f334b2747feeb907e281318fdf version: dd83d5cbcfd986f334b2747feeb907e281318fdf
- name: github.com/mitchellh/mapstructure - name: github.com/mitchellh/mapstructure
version: d2dd0262208475919e1a362f675cfc0e7c10e905 version: d2dd0262208475919e1a362f675cfc0e7c10e905
- name: github.com/moul/http2curl
version: 1812aee76a1ce98d604a44200c6a23c689b17a89
- name: github.com/opencontainers/runc - name: github.com/opencontainers/runc
version: 4ab132458fc3e9dbeea624153e0331952dc4c8d5 version: 4ab132458fc3e9dbeea624153e0331952dc4c8d5
subpackages: subpackages:
- libcontainer/user - libcontainer/user
- name: github.com/parnurzeal/gorequest
version: 91b42fce877cc6af96c45818665a4c615cc5f4ee
- name: github.com/pmezard/go-difflib - name: github.com/pmezard/go-difflib
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
subpackages: subpackages:
@ -192,6 +208,8 @@ imports:
version: fa6674abf3f4580b946a01bf7a1ce4ba8766205b version: fa6674abf3f4580b946a01bf7a1ce4ba8766205b
subpackages: subpackages:
- zk - zk
- name: github.com/Shopify/sarama
version: 92a286e4dde1688175cff3d2ec9b49a02838b447
- name: github.com/Sirupsen/logrus - name: github.com/Sirupsen/logrus
version: 418b41d23a1bf978c06faea5313ba194650ac088 version: 418b41d23a1bf978c06faea5313ba194650ac088
- name: github.com/spf13/cast - name: github.com/spf13/cast
@ -217,6 +235,10 @@ imports:
- assert - assert
- name: github.com/thoas/stats - name: github.com/thoas/stats
version: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8 version: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8
- name: github.com/ugorji/go
version: ea9cd21fa0bc41ee4bdd50ac7ed8cbc7ea2ed960
subpackages:
- codec
- name: github.com/unrolled/render - name: github.com/unrolled/render
version: 26b4e3aac686940fe29521545afad9966ddfc80c version: 26b4e3aac686940fe29521545afad9966ddfc80c
- name: github.com/vdemeester/docker-events - name: github.com/vdemeester/docker-events

View file

@ -1,177 +1,187 @@
package: main package: main
import: import:
- package: github.com/coreos/go-etcd - package: github.com/coreos/etcd
ref: cc90c7b091275e606ad0ca7102a23fb2072f3f5e version: 26e52d2bce9e3e11b77b68cc84bf91aebb1ef637
subpackages: subpackages:
- etcd - client
- package: github.com/mailgun/log - package: github.com/mailgun/log
ref: 44874009257d4d47ba9806f1b7f72a32a015e4d8 version: 44874009257d4d47ba9806f1b7f72a32a015e4d8
- package: github.com/containous/oxy - package: github.com/containous/oxy
ref: 021f82bd8260ba15f5862a9fe62018437720dff5 version: 021f82bd8260ba15f5862a9fe62018437720dff5
subpackages: subpackages:
- cbreaker - cbreaker
- forward - forward
- memmetrics - memmetrics
- roundrobin - roundrobin
- utils - utils
- package: github.com/hashicorp/consul - package: github.com/hashicorp/consul
ref: de080672fee9e6104572eeea89eccdca135bb918 version: de080672fee9e6104572eeea89eccdca135bb918
subpackages: subpackages:
- api - api
- package: github.com/samuel/go-zookeeper - package: github.com/samuel/go-zookeeper
ref: fa6674abf3f4580b946a01bf7a1ce4ba8766205b version: fa6674abf3f4580b946a01bf7a1ce4ba8766205b
subpackages: subpackages:
- zk - zk
- package: github.com/docker/libtrust - package: github.com/docker/libtrust
ref: 9cbd2a1374f46905c68a4eb3694a130610adc62a version: 9cbd2a1374f46905c68a4eb3694a130610adc62a
- package: github.com/go-check/check - package: github.com/go-check/check
ref: 11d3bc7aa68e238947792f30573146a3231fc0f1 version: 11d3bc7aa68e238947792f30573146a3231fc0f1
- package: golang.org/x/net - package: golang.org/x/net
ref: d9558e5c97f85372afee28cf2b6059d7d3818919 version: d9558e5c97f85372afee28cf2b6059d7d3818919
subpackages: subpackages:
- context - context
- package: github.com/gorilla/handlers - package: github.com/gorilla/handlers
ref: 40694b40f4a928c062f56849989d3e9cd0570e5f version: 40694b40f4a928c062f56849989d3e9cd0570e5f
- package: github.com/docker/libkv - package: github.com/docker/libkv
ref: 3732f7ff1b56057c3158f10bceb1e79133025373 version: 7283ef27ed32fe267388510a91709b307bb9942c
- package: github.com/alecthomas/template - package: github.com/alecthomas/template
ref: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0 version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0
- package: github.com/vdemeester/shakers - package: github.com/vdemeester/shakers
ref: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3 version: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3
- package: github.com/alecthomas/units - package: github.com/alecthomas/units
ref: 6b4e7dc5e3143b85ea77909c72caf89416fc2915 version: 6b4e7dc5e3143b85ea77909c72caf89416fc2915
- package: github.com/gambol99/go-marathon - package: github.com/gambol99/go-marathon
ref: ade11d1dc2884ee1f387078fc28509559b6235d1 version: ade11d1dc2884ee1f387078fc28509559b6235d1
- package: github.com/vulcand/predicate - package: github.com/vulcand/predicate
ref: cb0bff91a7ab7cf7571e661ff883fc997bc554a3 version: cb0bff91a7ab7cf7571e661ff883fc997bc554a3
- package: github.com/thoas/stats - package: github.com/thoas/stats
ref: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8 version: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8
- package: github.com/Sirupsen/logrus - package: github.com/Sirupsen/logrus
ref: 418b41d23a1bf978c06faea5313ba194650ac088 version: 418b41d23a1bf978c06faea5313ba194650ac088
- package: github.com/unrolled/render - package: github.com/unrolled/render
ref: 26b4e3aac686940fe29521545afad9966ddfc80c version: 26b4e3aac686940fe29521545afad9966ddfc80c
- package: github.com/flynn/go-shlex - package: github.com/flynn/go-shlex
ref: 3f9db97f856818214da2e1057f8ad84803971cff version: 3f9db97f856818214da2e1057f8ad84803971cff
- package: github.com/boltdb/bolt - package: github.com/boltdb/bolt
ref: 51f99c862475898df9773747d3accd05a7ca33c1 version: 51f99c862475898df9773747d3accd05a7ca33c1
- package: gopkg.in/mgo.v2 - package: gopkg.in/mgo.v2
ref: 22287bab4379e1fbf6002fb4eb769888f3fb224c version: 22287bab4379e1fbf6002fb4eb769888f3fb224c
subpackages: subpackages:
- bson - bson
- package: github.com/docker/docker - package: github.com/docker/docker
ref: f39987afe8d611407887b3094c03d6ba6a766a67 version: f39987afe8d611407887b3094c03d6ba6a766a67
subpackages: subpackages:
- autogen - autogen
- api - api
- cliconfig - cliconfig
- daemon/network - daemon/network
- graph/tags - graph/tags
- image - image
- opts - opts
- pkg/archive - pkg/archive
- pkg/fileutils - pkg/fileutils
- pkg/homedir - pkg/homedir
- pkg/httputils - pkg/httputils
- pkg/ioutils - pkg/ioutils
- pkg/jsonmessage - pkg/jsonmessage
- pkg/mflag - pkg/mflag
- pkg/nat - pkg/nat
- pkg/parsers - pkg/parsers
- pkg/pools - pkg/pools
- pkg/promise - pkg/promise
- pkg/random - pkg/random
- pkg/stdcopy - pkg/stdcopy
- pkg/stringid - pkg/stringid
- pkg/symlink - pkg/symlink
- pkg/system - pkg/system
- pkg/tarsum - pkg/tarsum
- pkg/term - pkg/term
- pkg/timeutils - pkg/timeutils
- pkg/tlsconfig - pkg/tlsconfig
- pkg/ulimit - pkg/ulimit
- pkg/units - pkg/units
- pkg/urlutil - pkg/urlutil
- pkg/useragent - pkg/useragent
- pkg/version - pkg/version
- registry - registry
- runconfig - runconfig
- utils - utils
- volume - volume
- package: github.com/mailgun/timetools - package: github.com/mailgun/timetools
ref: fd192d755b00c968d312d23f521eb0cdc6f66bd0 version: fd192d755b00c968d312d23f521eb0cdc6f66bd0
- package: github.com/codegangsta/negroni - package: github.com/codegangsta/negroni
ref: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b
- package: gopkg.in/yaml.v2 - package: gopkg.in/yaml.v2
ref: 7ad95dd0798a40da1ccdff6dff35fd177b5edf40 version: 7ad95dd0798a40da1ccdff6dff35fd177b5edf
- package: github.com/opencontainers/runc - package: github.com/opencontainers/runc
ref: 4ab132458fc3e9dbeea624153e0331952dc4c8d5 version: 4ab132458fc3e9dbeea624153e0331952dc4c8d5
subpackages: subpackages:
- libcontainer/user - libcontainer/user
- package: github.com/gorilla/mux - package: github.com/gorilla/mux
ref: f15e0c49460fd49eebe2bcc8486b05d1bef68d3a version: f15e0c49460fd49eebe2bcc8486b05d1bef68d3a
- package: github.com/BurntSushi/ty - package: github.com/BurntSushi/ty
ref: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74 version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74
- package: github.com/elazarl/go-bindata-assetfs - package: github.com/elazarl/go-bindata-assetfs
ref: d5cac425555ca5cf00694df246e04f05e6a55150 version: d5cac425555ca5cf00694df246e04f05e6a55150
- package: github.com/BurntSushi/toml - package: github.com/BurntSushi/toml
ref: bd2bdf7f18f849530ef7a1c29a4290217cab32a1 version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f
- package: gopkg.in/alecthomas/kingpin.v2 - package: gopkg.in/alecthomas/kingpin.v2
ref: 639879d6110b1b0409410c7b737ef0bb18325038 version: 639879d6110b1b0409410c7b737ef0bb18325038
- package: github.com/cenkalti/backoff - package: github.com/cenkalti/backoff
ref: 4dc77674aceaabba2c7e3da25d4c823edfb73f99 version: 4dc77674aceaabba2c7e3da25d4c823edfb73f99
- package: gopkg.in/fsnotify.v1 - package: gopkg.in/fsnotify.v1
ref: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0 version: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0
- package: github.com/mailgun/manners - package: github.com/mailgun/manners
ref: fada45142db3f93097ca917da107aa3fad0ffcb5 version: fada45142db3f93097ca917da107aa3fad0ffcb5
- package: github.com/gorilla/context - package: github.com/gorilla/context
ref: 215affda49addc4c8ef7e2534915df2c8c35c6cd version: 215affda49addc4c8ef7e2534915df2c8c35c6cd
- package: github.com/codahale/hdrhistogram - package: github.com/codahale/hdrhistogram
ref: 954f16e8b9ef0e5d5189456aa4c1202758e04f17 version: 954f16e8b9ef0e5d5189456aa4c1202758e04f17
- package: github.com/gorilla/websocket - package: github.com/gorilla/websocket
- package: github.com/donovanhide/eventsource - package: github.com/donovanhide/eventsource
ref: d8a3071799b98cacd30b6da92f536050ccfe6da4 version: d8a3071799b98cacd30b6da92f536050ccfe6da4
- package: github.com/golang/glog - package: github.com/golang/glog
ref: fca8c8854093a154ff1eb580aae10276ad6b1b5f version: fca8c8854093a154ff1eb580aae10276ad6b1b5f
- package: github.com/spf13/cast - package: github.com/spf13/cast
ref: ee7b3e0353166ab1f3a605294ac8cd2b77953778 version: ee7b3e0353166ab1f3a605294ac8cd2b77953778
- package: github.com/mitchellh/mapstructure - package: github.com/mitchellh/mapstructure
- package: github.com/spf13/jwalterweatherman - package: github.com/spf13/jwalterweatherman
- package: github.com/spf13/pflag - package: github.com/spf13/pflag
- package: github.com/wendal/errors - package: github.com/wendal/errors
- package: github.com/hashicorp/hcl - package: github.com/hashicorp/hcl
- package: github.com/kr/pretty - package: github.com/kr/pretty
- package: github.com/magiconair/properties - package: github.com/magiconair/properties
- package: github.com/kr/text - package: github.com/kr/text
- package: github.com/spf13/viper - package: github.com/spf13/viper
ref: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325 version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325
- package: github.com/spf13/cobra - package: github.com/spf13/cobra
subpackages: subpackages:
- /cobra - cobra
- package: github.com/google/go-querystring/query - package: github.com/google/go-querystring
- package: github.com/vulcand/vulcand/plugin/rewrite subpackages:
- package: github.com/stretchr/testify/mock - query
- package: github.com/xenolf/lego - package: github.com/vulcand/vulcand
- package: github.com/vdemeester/libkermit subpackages:
ref: 7e4e689a6fa9281e0fb9b7b9c297e22d5342a5ec - plugin/rewrite
- package: github.com/docker/libcompose - package: github.com/stretchr/testify
version: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a subpackages:
- package: github.com/docker/distribution - mock
version: ff6f38ccb69afa96214c7ee955359465d1fc767a - package: github.com/xenolf/lego
subpackages: - package: github.com/vdemeester/libkermit
- reference version: 7e4e689a6fa9281e0fb9b7b9c297e22d5342a5ec
- package: github.com/docker/engine-api - package: github.com/docker/libcompose
subpackages: version: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a
- client - package: github.com/docker/distribution
- types version: ff6f38ccb69afa96214c7ee955359465d1fc767a
- types/container subpackages:
- types/filters - reference
- types/strslice - package: github.com/docker/engine-api
- package: github.com/vdemeester/docker-events version: 8924d6900370b4c7e7984be5adc61f50a80d7537
- package: github.com/docker/go-connections subpackages:
subpackages: - client
- nat - types
- sockets - types/container
- tlsconfig - types/filters
- package: github.com/docker/go-units - types/strslice
- package: github.com/mailgun/multibuf - package: github.com/vdemeester/docker-events
- package: github.com/streamrail/concurrent-map - 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
- package: github.com/mattn/go-shellwords
- package: github.com/moul/http2curl

View file

@ -0,0 +1,106 @@
package main
import (
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"regexp"
"strings"
"time"
"github.com/go-check/check"
shellwords "github.com/mattn/go-shellwords"
checker "github.com/vdemeester/shakers"
)
// AccessLogSuite
type AccessLogSuite struct{ BaseSuite }
func (s *AccessLogSuite) TestAccessLog(c *check.C) {
// Ensure working directory is clean
os.Remove("access.log")
os.Remove("traefik.log")
// Start Traefik
cmd := exec.Command(traefikBinary, "--configFile=fixtures/access_log_config.toml")
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
defer os.Remove("access.log")
defer os.Remove("traefik.log")
time.Sleep(500 * time.Millisecond)
// Verify Traefik started OK
traefikLog, err := ioutil.ReadFile("traefik.log")
c.Assert(err, checker.IsNil)
if len(traefikLog) > 0 {
fmt.Printf("%s\n", string(traefikLog))
c.Assert(len(traefikLog), checker.Equals, 0)
}
// Start test servers
ts1 := startAccessLogServer(8081)
defer ts1.Close()
ts2 := startAccessLogServer(8082)
defer ts2.Close()
ts3 := startAccessLogServer(8083)
defer ts3.Close()
// Make some requests
_, err = http.Get("http://127.0.0.1:8000/test1")
c.Assert(err, checker.IsNil)
_, err = http.Get("http://127.0.0.1:8000/test2")
c.Assert(err, checker.IsNil)
_, err = http.Get("http://127.0.0.1:8000/test2")
c.Assert(err, checker.IsNil)
// Verify access.log output as expected
accessLog, err := ioutil.ReadFile("access.log")
c.Assert(err, checker.IsNil)
lines := strings.Split(string(accessLog), "\n")
count := 0
for i, line := range lines {
if len(line) > 0 {
count++
tokens, err := shellwords.Parse(line)
c.Assert(err, checker.IsNil)
c.Assert(len(tokens), checker.Equals, 13)
c.Assert(tokens[6], checker.Equals, "200")
c.Assert(tokens[9], checker.Equals, fmt.Sprintf("%d", i+1))
c.Assert(strings.HasPrefix(tokens[10], "frontend"), checker.True)
c.Assert(strings.HasPrefix(tokens[11], "http://127.0.0.1:808"), checker.True)
c.Assert(regexp.MustCompile("^\\d+\\.\\d+.*s$").MatchString(tokens[12]), checker.True)
}
}
c.Assert(count, checker.Equals, 3)
// Verify no other Traefik problems
traefikLog, err = ioutil.ReadFile("traefik.log")
c.Assert(err, checker.IsNil)
if len(traefikLog) > 0 {
fmt.Printf("%s\n", string(traefikLog))
c.Assert(len(traefikLog), checker.Equals, 0)
}
}
func startAccessLogServer(port int) (ts *httptest.Server) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Received query %s!\n", r.URL.Path[1:])
})
if listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)); err != nil {
panic(err)
} else {
ts = &httptest.Server{
Listener: listener,
Config: &http.Server{Handler: handler},
}
ts.Start()
}
return
}

View file

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"github.com/go-check/check" "github.com/go-check/check"
"bytes"
checker "github.com/vdemeester/shakers" checker "github.com/vdemeester/shakers"
) )
@ -16,25 +17,45 @@ type SimpleSuite struct{ BaseSuite }
func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) { func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) {
cmd := exec.Command(traefikBinary) cmd := exec.Command(traefikBinary)
output, err := cmd.CombinedOutput()
c.Assert(err, checker.NotNil) var b bytes.Buffer
c.Assert(string(output), checker.Contains, "Error reading file: open : no such file or directory") 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" nonExistentFile := "non/existent/file.toml"
cmd = exec.Command(traefikBinary, "--configFile="+nonExistentFile) cmd = exec.Command(traefikBinary, "--configFile="+nonExistentFile)
output, err = cmd.CombinedOutput()
c.Assert(err, checker.NotNil) cmd.Stdout = &b
c.Assert(string(output), checker.Contains, fmt.Sprintf("Error reading file: open %s: no such file or directory", nonExistentFile)) 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) { func (s *SimpleSuite) TestInvalidConfigShouldFail(c *check.C) {
cmd := exec.Command(traefikBinary, "--configFile=fixtures/invalid_configuration.toml") cmd := exec.Command(traefikBinary, "--configFile=fixtures/invalid_configuration.toml")
output, err := cmd.CombinedOutput()
c.Assert(err, checker.NotNil) var b bytes.Buffer
c.Assert(string(output), checker.Contains, "Error reading file: While parsing config: Near line 1") 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) { func (s *SimpleSuite) TestSimpleDefaultConfig(c *check.C) {

View file

@ -0,0 +1,46 @@
################################################################
# Global configuration
################################################################
traefikLogsFile = "traefik.log"
accessLogsFile = "access.log"
logLevel = "ERROR"
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":8000"
################################################################
# Web configuration backend
################################################################
[web]
address = ":7888"
################################################################
# File configuration backend
################################################################
[file]
################################################################
# rules
################################################################
[backends]
[backends.backend1]
[backends.backend1.servers.server1]
url = "http://127.0.0.1:8081"
[backends.backend2]
[backends.backend2.LoadBalancer]
method = "drr"
[backends.backend2.servers.server1]
url = "http://127.0.0.1:8082"
[backends.backend2.servers.server2]
url = "http://127.0.0.1:8083"
[frontends]
[frontends.frontend1]
backend = "backend1"
[frontends.frontend1.routes.test_1]
rule = "Path: /test1"
[frontends.frontend2]
backend = "backend2"
passHostHeader = true
[frontends.frontend2.routes.test_2]
rule = "Path: /test2"

View file

@ -23,6 +23,7 @@ func Test(t *testing.T) {
func init() { func init() {
check.Suite(&SimpleSuite{}) check.Suite(&SimpleSuite{})
check.Suite(&AccessLogSuite{})
check.Suite(&HTTPSSuite{}) check.Suite(&HTTPSSuite{})
check.Suite(&FileSuite{}) check.Suite(&FileSuite{})
check.Suite(&DockerSuite{}) check.Suite(&DockerSuite{})

View file

@ -1,18 +1,54 @@
package middlewares package middlewares
import ( import (
"log" "fmt"
log "github.com/Sirupsen/logrus"
"github.com/streamrail/concurrent-map"
"io"
"net"
"net/http" "net/http"
"os" "os"
"strconv"
"github.com/gorilla/handlers" "strings"
"sync/atomic"
"time"
) )
// Logger is a middleware handler that logs the request as it goes in and the response as it goes out. const (
loggerReqidHeader = "X-Traefik-Reqid"
)
/*
Logger writes each request and its response to the access log.
It gets some information from the logInfoResponseWriter set up by previous middleware.
*/
type Logger struct { type Logger struct {
file *os.File file *os.File
} }
// Logging handler to log frontend name, backend name, and elapsed time
type frontendBackendLoggingHandler struct {
reqid string
writer io.Writer
handlerFunc http.HandlerFunc
}
var (
reqidCounter uint64 // Request ID
infoRwMap = cmap.New() // Map of reqid to response writer
backend2FrontendMap *map[string]string
)
// logInfoResponseWriter is a wrapper of type http.ResponseWriter
// that tracks frontend and backend names and request status and size
type logInfoResponseWriter struct {
rw http.ResponseWriter
backend string
frontend string
status int
size int
}
// NewLogger returns a new Logger instance. // NewLogger returns a new Logger instance.
func NewLogger(file string) *Logger { func NewLogger(file string) *Logger {
if len(file) > 0 { if len(file) > 0 {
@ -25,17 +61,132 @@ func NewLogger(file string) *Logger {
return &Logger{nil} return &Logger{nil}
} }
// SetBackend2FrontendMap is called by server.go to set up frontend translation
func SetBackend2FrontendMap(newMap *map[string]string) {
backend2FrontendMap = newMap
}
func (l *Logger) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { func (l *Logger) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
if l.file == nil { if l.file == nil {
next(rw, r) next(rw, r)
} else { } else {
handlers.CombinedLoggingHandler(l.file, next).ServeHTTP(rw, r) reqid := strconv.FormatUint(atomic.AddUint64(&reqidCounter, 1), 10)
r.Header[loggerReqidHeader] = []string{reqid}
defer deleteReqid(r, reqid)
frontendBackendLoggingHandler{reqid, l.file, next}.ServeHTTP(rw, r)
} }
} }
// Close closes the logger (i.e. the file). // Delete a reqid from the map and the request's headers
func deleteReqid(r *http.Request, reqid string) {
infoRwMap.Remove(reqid)
delete(r.Header, loggerReqidHeader)
}
// Save the backend name for the Logger
func saveBackendNameForLogger(r *http.Request, backendName string) {
if reqidHdr := r.Header[loggerReqidHeader]; len(reqidHdr) == 1 {
reqid := reqidHdr[0]
if infoRw, ok := infoRwMap.Get(reqid); ok {
infoRw.(*logInfoResponseWriter).SetBackend(backendName)
infoRw.(*logInfoResponseWriter).SetFrontend((*backend2FrontendMap)[backendName])
}
}
}
// Close closes the Logger (i.e. the file).
func (l *Logger) Close() { func (l *Logger) Close() {
if l.file != nil { if l.file != nil {
l.file.Close() l.file.Close()
} }
} }
// Logging handler to log frontend name, backend name, and elapsed time
func (fblh frontendBackendLoggingHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
startTime := time.Now()
infoRw := &logInfoResponseWriter{rw: rw}
infoRwMap.Set(fblh.reqid, infoRw)
fblh.handlerFunc(infoRw, req)
username := "-"
url := *req.URL
if url.User != nil {
if name := url.User.Username(); name != "" {
username = name
}
}
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
host = req.RemoteAddr
}
ts := startTime.Format("02/Jan/2006:15:04:05 -0700")
method := req.Method
uri := url.RequestURI()
if qmIndex := strings.Index(uri, "?"); qmIndex > 0 {
uri = uri[0:qmIndex]
}
proto := req.Proto
referer := req.Referer()
agent := req.UserAgent()
frontend := strings.TrimPrefix(infoRw.GetFrontend(), "frontend-")
backend := infoRw.GetBackend()
status := infoRw.GetStatus()
size := infoRw.GetSize()
elapsed := time.Now().UTC().Sub(startTime.UTC())
fmt.Fprintf(fblh.writer, `%s - %s [%s] "%s %s %s" %d %d "%s" "%s" %s "%s" "%s" %s%s`,
host, username, ts, method, uri, proto, status, size, referer, agent, fblh.reqid, frontend, backend, elapsed, "\n")
}
func (lirw *logInfoResponseWriter) Header() http.Header {
return lirw.rw.Header()
}
func (lirw *logInfoResponseWriter) Write(b []byte) (int, error) {
if lirw.status == 0 {
lirw.status = http.StatusOK
}
size, err := lirw.rw.Write(b)
lirw.size += size
return size, err
}
func (lirw *logInfoResponseWriter) WriteHeader(s int) {
lirw.rw.WriteHeader(s)
lirw.status = s
}
func (lirw *logInfoResponseWriter) Flush() {
f, ok := lirw.rw.(http.Flusher)
if ok {
f.Flush()
}
}
func (lirw *logInfoResponseWriter) GetStatus() int {
return lirw.status
}
func (lirw *logInfoResponseWriter) GetSize() int {
return lirw.size
}
func (lirw *logInfoResponseWriter) GetBackend() string {
return lirw.backend
}
func (lirw *logInfoResponseWriter) GetFrontend() string {
return lirw.frontend
}
func (lirw *logInfoResponseWriter) SetBackend(backend string) {
lirw.backend = backend
}
func (lirw *logInfoResponseWriter) SetFrontend(frontend string) {
lirw.frontend = frontend
}

116
middlewares/logger_test.go Normal file
View file

@ -0,0 +1,116 @@
package middlewares
import (
"fmt"
shellwords "github.com/mattn/go-shellwords"
"github.com/stretchr/testify/assert"
"io/ioutil"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"testing"
)
type logtestResponseWriter struct{}
var (
logger *Logger
logfileName = "traefikTestLogger.log"
logfilePath string
helloWorld = "Hello, World"
testBackendName = "http://127.0.0.1/testBackend"
testFrontendName = "testFrontend"
testStatus = 123
testHostname = "TestHost"
testUsername = "TestUser"
testPath = "http://testpath"
testPort = 8181
testProto = "HTTP/0.0"
testMethod = "POST"
testReferer = "testReferer"
testUserAgent = "testUserAgent"
testBackend2FrontendMap = map[string]string{
testBackendName: testFrontendName,
}
printedLogdata bool
)
func TestLogger(t *testing.T) {
if runtime.GOOS == "windows" {
logfilePath = filepath.Join(os.Getenv("TEMP"), logfileName)
} else {
logfilePath = filepath.Join("/tmp", logfileName)
}
logger = NewLogger(logfilePath)
defer cleanup()
SetBackend2FrontendMap(&testBackend2FrontendMap)
r := &http.Request{
Header: map[string][]string{
"User-Agent": {testUserAgent},
"Referer": {testReferer},
},
Proto: testProto,
Host: testHostname,
Method: testMethod,
RemoteAddr: fmt.Sprintf("%s:%d", testHostname, testPort),
URL: &url.URL{
User: url.UserPassword(testUsername, ""),
Path: testPath,
},
}
logger.ServeHTTP(&logtestResponseWriter{}, r, LogWriterTestHandlerFunc)
if logdata, err := ioutil.ReadFile(logfilePath); err != nil {
fmt.Printf("%s\n%s\n", string(logdata), err.Error())
assert.Nil(t, err)
} else if tokens, err := shellwords.Parse(string(logdata)); err != nil {
fmt.Printf("%s\n", err.Error())
assert.Nil(t, err)
} else if assert.Equal(t, 14, len(tokens), printLogdata(logdata)) {
assert.Equal(t, testHostname, tokens[0], printLogdata(logdata))
assert.Equal(t, testUsername, tokens[2], printLogdata(logdata))
assert.Equal(t, fmt.Sprintf("%s %s %s", testMethod, testPath, testProto), tokens[5], printLogdata(logdata))
assert.Equal(t, fmt.Sprintf("%d", testStatus), tokens[6], printLogdata(logdata))
assert.Equal(t, fmt.Sprintf("%d", len(helloWorld)), tokens[7], printLogdata(logdata))
assert.Equal(t, testReferer, tokens[8], printLogdata(logdata))
assert.Equal(t, testUserAgent, tokens[9], printLogdata(logdata))
assert.Equal(t, "1", tokens[10], printLogdata(logdata))
assert.Equal(t, testFrontendName, tokens[11], printLogdata(logdata))
assert.Equal(t, testBackendName, tokens[12], printLogdata(logdata))
}
}
func cleanup() {
logger.Close()
os.Remove(logfilePath)
}
func printLogdata(logdata []byte) string {
return fmt.Sprintf(
"\nExpected: %s\n"+
"Actual: %s",
"TestHost - TestUser [13/Apr/2016:07:14:19 -0700] \"POST http://testpath HTTP/0.0\" 123 12 \"testReferer\" \"testUserAgent\" 1 \"testFrontend\" \"http://127.0.0.1/testBackend\" 1ms",
string(logdata))
}
func LogWriterTestHandlerFunc(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte(helloWorld))
rw.WriteHeader(testStatus)
saveBackendNameForLogger(r, testBackendName)
}
func (lrw *logtestResponseWriter) Header() http.Header {
return map[string][]string{}
}
func (lrw *logtestResponseWriter) Write(b []byte) (int, error) {
return len(b), nil
}
func (lrw *logtestResponseWriter) WriteHeader(s int) {
}

View file

@ -0,0 +1,20 @@
package middlewares
import (
"net/http"
)
// SaveBackend sends the backend name to the logger.
type SaveBackend struct {
next http.Handler
}
// NewSaveBackend creates a SaveBackend
func NewSaveBackend(next http.Handler) *SaveBackend {
return &SaveBackend{next}
}
func (sb *SaveBackend) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
saveBackendNameForLogger(r, (*r.URL).String())
sb.next.ServeHTTP(rw, r)
}

View file

@ -2,6 +2,7 @@ package provider
import ( import (
"errors" "errors"
"strconv"
"strings" "strings"
"text/template" "text/template"
"time" "time"
@ -123,10 +124,30 @@ func (provider *ConsulCatalog) getFrontendRule(service serviceUpdate) string {
return "Host:" + service.ServiceName + "." + provider.Domain return "Host:" + service.ServiceName + "." + provider.Domain
} }
func (provider *ConsulCatalog) getBackendAddress(node *api.ServiceEntry) string {
if node.Service.Address != "" {
return node.Service.Address
}
return node.Node.Address
}
func (provider *ConsulCatalog) getBackendName(node *api.ServiceEntry, index int) string {
serviceName := node.Service.Service + "--" + node.Service.Address + "--" + strconv.Itoa(node.Service.Port)
if len(node.Service.Tags) > 0 {
serviceName += "--" + strings.Join(node.Service.Tags, "--")
}
serviceName = strings.Replace(serviceName, ".", "-", -1)
serviceName = strings.Replace(serviceName, "=", "-", -1)
// unique int at the end
serviceName += "--" + strconv.Itoa(index)
return serviceName
}
func (provider *ConsulCatalog) getAttribute(name string, tags []string, defaultValue string) string { func (provider *ConsulCatalog) getAttribute(name string, tags []string, defaultValue string) string {
for _, tag := range tags { for _, tag := range tags {
if strings.Index(tag, DefaultConsulCatalogTagPrefix+".") == 0 { if strings.Index(strings.ToLower(tag), DefaultConsulCatalogTagPrefix+".") == 0 {
if kv := strings.SplitN(tag[len(DefaultConsulCatalogTagPrefix+"."):], "=", 2); len(kv) == 2 && kv[0] == name { if kv := strings.SplitN(tag[len(DefaultConsulCatalogTagPrefix+"."):], "=", 2); len(kv) == 2 && strings.ToLower(kv[0]) == strings.ToLower(name) {
return kv[1] return kv[1]
} }
} }
@ -136,19 +157,25 @@ func (provider *ConsulCatalog) getAttribute(name string, tags []string, defaultV
func (provider *ConsulCatalog) buildConfig(catalog []catalogUpdate) *types.Configuration { func (provider *ConsulCatalog) buildConfig(catalog []catalogUpdate) *types.Configuration {
var FuncMap = template.FuncMap{ var FuncMap = template.FuncMap{
"getBackend": provider.getBackend, "getBackend": provider.getBackend,
"getFrontendRule": provider.getFrontendRule, "getFrontendRule": provider.getFrontendRule,
"getAttribute": provider.getAttribute, "getBackendName": provider.getBackendName,
"getEntryPoints": provider.getEntryPoints, "getBackendAddress": provider.getBackendAddress,
"replace": replace, "getAttribute": provider.getAttribute,
"getEntryPoints": provider.getEntryPoints,
} }
allNodes := []*api.ServiceEntry{} allNodes := []*api.ServiceEntry{}
services := []*serviceUpdate{} services := []*serviceUpdate{}
for _, info := range catalog { for _, info := range catalog {
if len(info.Nodes) > 0 { for _, node := range info.Nodes {
services = append(services, info.Service) isEnabled := provider.getAttribute("enable", node.Service.Tags, "true")
allNodes = append(allNodes, info.Nodes...) if isEnabled != "false" && len(info.Nodes) > 0 {
services = append(services, info.Service)
allNodes = append(allNodes, info.Nodes...)
break
}
} }
} }

View file

@ -82,6 +82,88 @@ func TestConsulCatalogGetAttribute(t *testing.T) {
} }
} }
func TestConsulCatalogGetBackendAddress(t *testing.T) {
provider := &ConsulCatalog{
Domain: "localhost",
}
services := []struct {
node *api.ServiceEntry
expected string
}{
{
node: &api.ServiceEntry{
Node: &api.Node{
Address: "10.1.0.1",
},
Service: &api.AgentService{
Address: "10.2.0.1",
},
},
expected: "10.2.0.1",
},
{
node: &api.ServiceEntry{
Node: &api.Node{
Address: "10.1.0.1",
},
Service: &api.AgentService{
Address: "",
},
},
expected: "10.1.0.1",
},
}
for _, e := range services {
actual := provider.getBackendAddress(e.node)
if actual != e.expected {
t.Fatalf("expected %q, got %q", e.expected, actual)
}
}
}
func TestConsulCatalogGetBackendName(t *testing.T) {
provider := &ConsulCatalog{
Domain: "localhost",
}
services := []struct {
node *api.ServiceEntry
expected string
}{
{
node: &api.ServiceEntry{
Service: &api.AgentService{
Service: "api",
Address: "10.0.0.1",
Port: 80,
Tags: []string{},
},
},
expected: "api--10-0-0-1--80--0",
},
{
node: &api.ServiceEntry{
Service: &api.AgentService{
Service: "api",
Address: "10.0.0.1",
Port: 80,
Tags: []string{"traefik.weight=42", "traefik.enable=true"},
},
},
expected: "api--10-0-0-1--80--traefik-weight-42--traefik-enable-true--1",
},
}
for i, e := range services {
actual := provider.getBackendName(e.node, i)
if actual != e.expected {
t.Fatalf("expected %q, got %q", e.expected, actual)
}
}
}
func TestConsulCatalogBuildConfig(t *testing.T) { func TestConsulCatalogBuildConfig(t *testing.T) {
provider := &ConsulCatalog{ provider := &ConsulCatalog{
Domain: "localhost", Domain: "localhost",
@ -154,7 +236,7 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
expectedBackends: map[string]*types.Backend{ expectedBackends: map[string]*types.Backend{
"backend-test": { "backend-test": {
Servers: map[string]types.Server{ Servers: map[string]types.Server{
"test--127-0-0-1--80": { "test--127-0-0-1--80--traefik-backend-weight-42--random-foo-bar--traefik-backend-passHostHeader-true--traefik-protocol-https--0": {
URL: "https://127.0.0.1:80", URL: "https://127.0.0.1:80",
Weight: 42, Weight: 42,
}, },

View file

@ -743,11 +743,11 @@ func TestDockerLoadDockerConfig(t *testing.T) {
}, },
}, },
expectedFrontends: map[string]*types.Frontend{ expectedFrontends: map[string]*types.Frontend{
`"frontend-Host-test-docker-localhost"`: { "frontend-Host-test-docker-localhost": {
Backend: "backend-test", Backend: "backend-test",
EntryPoints: []string{}, EntryPoints: []string{},
Routes: map[string]types.Route{ Routes: map[string]types.Route{
`"route-frontend-Host-test-docker-localhost"`: { "route-frontend-Host-test-docker-localhost": {
Rule: "Host:test.docker.localhost", Rule: "Host:test.docker.localhost",
}, },
}, },
@ -815,20 +815,20 @@ func TestDockerLoadDockerConfig(t *testing.T) {
}, },
}, },
expectedFrontends: map[string]*types.Frontend{ expectedFrontends: map[string]*types.Frontend{
`"frontend-Host-test1-docker-localhost"`: { "frontend-Host-test1-docker-localhost": {
Backend: "backend-foobar", Backend: "backend-foobar",
EntryPoints: []string{"http", "https"}, EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{ Routes: map[string]types.Route{
`"route-frontend-Host-test1-docker-localhost"`: { "route-frontend-Host-test1-docker-localhost": {
Rule: "Host:test1.docker.localhost", Rule: "Host:test1.docker.localhost",
}, },
}, },
}, },
`"frontend-Host-test2-docker-localhost"`: { "frontend-Host-test2-docker-localhost": {
Backend: "backend-foobar", Backend: "backend-foobar",
EntryPoints: []string{}, EntryPoints: []string{},
Routes: map[string]types.Route{ Routes: map[string]types.Route{
`"route-frontend-Host-test2-docker-localhost"`: { "route-frontend-Host-test2-docker-localhost": {
Rule: "Host:test2.docker.localhost", Rule: "Host:test2.docker.localhost",
}, },
}, },

274
provider/k8s/client.go Normal file
View file

@ -0,0 +1,274 @@
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"
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 ingresses 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 services 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 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 decode version %v", err)
}
resourceVersion := generic.ResourceVersion
url = url + "?watch&resourceVersion=" + resourceVersion
// Make request to Kubernetes API
request := c.request(url)
req, err := request.MakeRequest()
if err != nil {
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 do watch 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
}

151
provider/k8s/ingress.go Normal file
View file

@ -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://<host>/<path>?<searchpart> -> 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"`
}

326
provider/k8s/service.go Normal file
View file

@ -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"`
}

200
provider/kubernetes.go Normal file
View file

@ -0,0 +1,200 @@
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 {
protocol := "http"
for _, port := range service.Spec.Ports {
if port.Port == pa.Backend.ServicePort.IntValue() {
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,
}
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
}

187
provider/kubernetes_test.go Normal file
View file

@ -0,0 +1,187 @@
package provider
import (
"encoding/json"
"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(443),
},
},
{
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{
{
Port: 802,
},
},
},
},
{
ObjectMeta: k8s.ObjectMeta{
Name: "service3",
UID: "3",
},
Spec: k8s.ServiceSpec{
ClusterIP: "10.0.0.3",
Ports: []k8s.ServicePort{
{
Name: "http",
Port: 443,
},
},
},
},
}
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: "https://10.0.0.3:443",
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",
},
},
},
},
}
actualJSON, _ := json.Marshal(actual)
expectedJSON, _ := json.Marshal(expected)
if !reflect.DeepEqual(actual, expected) {
t.Fatalf("expected %+v, got %+v", string(expectedJSON), string(actualJSON))
}
}
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
}

View file

@ -10,8 +10,10 @@ import (
"text/template" "text/template"
"time" "time"
"errors"
"github.com/BurntSushi/ty/fun" "github.com/BurntSushi/ty/fun"
log "github.com/Sirupsen/logrus" log "github.com/Sirupsen/logrus"
"github.com/cenkalti/backoff"
"github.com/containous/traefik/safe" "github.com/containous/traefik/safe"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
"github.com/docker/libkv" "github.com/docker/libkv"
@ -37,25 +39,38 @@ type KvTLS struct {
} }
func (provider *Kv) watchKv(configurationChan chan<- types.ConfigMessage, prefix string, stop chan bool) { 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 */) events, err := provider.kvclient.WatchTree(provider.Prefix, make(chan struct{}) /* stop chan */)
if err != nil { if err != nil {
log.Errorf("Failed to WatchTree %s", err) log.Errorf("Failed to WatchTree %s", err)
continue return err
} }
select { for {
case <-stop: select {
return case <-stop:
case <-events: return nil
configuration := provider.loadConfig() case _, ok := <-events:
if configuration != nil { if !ok {
configurationChan <- types.ConfigMessage{ return errors.New("watchtree channel closed")
ProviderName: string(provider.storeType), }
Configuration: configuration, 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 { 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( operation := func() error {
provider.storeType, kv, err := libkv.NewStore(
strings.Split(provider.Endpoint, ","), provider.storeType,
storeConfig, 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 { if err != nil {
return err log.Fatalf("Cannot connect to KV server %+v", 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 return nil
} }

View file

@ -7,7 +7,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/containous/traefik/safe"
"github.com/docker/libkv/store" "github.com/docker/libkv/store"
"reflect" "reflect"
"sort" "sort"
@ -81,7 +80,7 @@ func TestKvList(t *testing.T) {
}, },
}, },
keys: []string{"foo", "/baz/"}, 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) configChan := make(chan types.ConfigMessage)
safe.Go(func() { go func() {
provider.watchKv(configChan, "prefix", make(chan bool, 1)) provider.watchKv(configChan, "prefix", make(chan bool, 1))
}) }()
select { select {
case c1 := <-returnedChans: case c1 := <-returnedChans:
@ -339,7 +338,7 @@ func (s *Mock) List(prefix string) ([]*store.KVPair, error) {
} }
kv := []*store.KVPair{} kv := []*store.KVPair{}
for _, kvPair := range s.KVPairs { 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) kv = append(kv, kvPair)
} }
} }
@ -365,3 +364,86 @@ func (s *Mock) AtomicDelete(key string, previous *store.KVPair) (bool, error) {
func (s *Mock) Close() { func (s *Mock) Close() {
return 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)
}
}

View file

@ -69,6 +69,7 @@ func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage,
return err return err
} }
pool.Go(func(stop chan bool) { pool.Go(func(stop chan bool) {
defer close(update)
for { for {
select { select {
case <-stop: case <-stop:
@ -86,6 +87,11 @@ func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage,
} }
}) })
} }
configuration := provider.loadMarathonConfig()
configurationChan <- types.ConfigMessage{
ProviderName: "marathon",
Configuration: configuration,
}
return nil return nil
} }

View file

@ -74,7 +74,7 @@ func TestConfigurationErrors(t *testing.T) {
Filename: templateInvalidTOMLFile.Name(), 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{ funcMap: template.FuncMap{
"Foo": func() string { "Foo": func() string {
return "bar" return "bar"

View file

@ -116,7 +116,7 @@ func (r *Rules) Parse(expression string) (*mux.Route, error) {
} }
parsedFunction, ok := functions[parsedFunctions[0]] parsedFunction, ok := functions[parsedFunctions[0]]
if !ok { 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:]...) parsedFunctions = append(parsedFunctions[:0], parsedFunctions[1:]...)
fargs := func(c rune) bool { fargs := func(c rune) bool {

View file

@ -22,4 +22,4 @@ if [ -z "$DATE" ]; then
fi fi
# Build binaries # 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 .

View file

@ -180,9 +180,9 @@ func (server *Server) listenConfigurations(stop chan bool) {
} }
currentConfigurations := server.currentConfigurations.Get().(configs) currentConfigurations := server.currentConfigurations.Get().(configs)
if configMsg.Configuration == nil { if configMsg.Configuration == nil {
log.Info("Skipping empty Configuration") log.Infof("Skipping empty Configuration for provider %s", configMsg.ProviderName)
} else if reflect.DeepEqual(currentConfigurations[configMsg.ProviderName], configMsg.Configuration) { } else if reflect.DeepEqual(currentConfigurations[configMsg.ProviderName], configMsg.Configuration) {
log.Info("Skipping same configuration") log.Infof("Skipping same configuration for provider %s", configMsg.ProviderName)
} else { } else {
// Copy configurations to new map so we don't change current if LoadConfig fails // Copy configurations to new map so we don't change current if LoadConfig fails
newConfigurations := make(configs) newConfigurations := make(configs)
@ -236,6 +236,9 @@ func (server *Server) configureProviders() {
if server.globalConfiguration.Boltdb != nil { if server.globalConfiguration.Boltdb != nil {
server.providers = append(server.providers, server.globalConfiguration.Boltdb) 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() { func (server *Server) startProviders() {
@ -369,6 +372,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
redirectHandlers := make(map[string]http.Handler) redirectHandlers := make(map[string]http.Handler)
backends := map[string]http.Handler{} backends := map[string]http.Handler{}
backend2FrontendMap := map[string]string{}
for _, configuration := range configurations { for _, configuration := range configurations {
frontendNames := sortedFrontendNamesForConfig(configuration) frontendNames := sortedFrontendNamesForConfig(configuration)
for _, frontendName := range frontendNames { for _, frontendName := range frontendNames {
@ -376,6 +380,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
log.Debugf("Creating frontend %s", frontendName) log.Debugf("Creating frontend %s", frontendName)
fwd, _ := forward.New(forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader)) fwd, _ := forward.New(forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader))
saveBackend := middlewares.NewSaveBackend(fwd)
// default endpoints if not defined in frontends // default endpoints if not defined in frontends
if len(frontend.EntryPoints) == 0 { if len(frontend.EntryPoints) == 0 {
frontend.EntryPoints = globalConfiguration.DefaultEntryPoints frontend.EntryPoints = globalConfiguration.DefaultEntryPoints
@ -411,7 +416,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
if backends[frontend.Backend] == nil { if backends[frontend.Backend] == nil {
log.Debugf("Creating backend %s", frontend.Backend) log.Debugf("Creating backend %s", frontend.Backend)
var lb http.Handler var lb http.Handler
rr, _ := roundrobin.New(fwd) rr, _ := roundrobin.New(saveBackend)
if configuration.Backends[frontend.Backend] == nil { if configuration.Backends[frontend.Backend] == nil {
return nil, errors.New("Undefined backend: " + frontend.Backend) return nil, errors.New("Undefined backend: " + frontend.Backend)
} }
@ -429,6 +434,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
if err != nil { if err != nil {
return nil, err return nil, err
} }
backend2FrontendMap[url.String()] = frontendName
log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight) log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight)
if err := rebalancer.UpsertServer(url, roundrobin.Weight(server.Weight)); err != nil { if err := rebalancer.UpsertServer(url, roundrobin.Weight(server.Weight)); err != nil {
return nil, err return nil, err
@ -442,6 +448,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
if err != nil { if err != nil {
return nil, err return nil, err
} }
backend2FrontendMap[url.String()] = frontendName
log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight) log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight)
if err := rr.UpsertServer(url, roundrobin.Weight(server.Weight)); err != nil { if err := rr.UpsertServer(url, roundrobin.Weight(server.Weight)); err != nil {
return nil, err return nil, err
@ -503,6 +510,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
} }
} }
} }
middlewares.SetBackend2FrontendMap(&backend2FrontendMap)
return serverEntryPoints, nil return serverEntryPoints, nil
} }

View file

@ -1,9 +1,9 @@
[backends] [backends]
{{range .Nodes}} {{range $index, $node := .Nodes}}
{{if ne (getAttribute "enable" .Service.Tags "true") "false"}} {{if ne (getAttribute "enable" $node.Service.Tags "true") "false"}}
[backends.backend-{{getBackend .}}.servers.{{.Service.Service | replace "." "-"}}--{{.Service.Address | replace "." "-"}}--{{.Service.Port}}] [backends.backend-{{getBackend $node}}.servers.{{getBackendName $node $index}}]
url = "{{getAttribute "protocol" .Service.Tags "http"}}://{{.Service.Address}}:{{.Service.Port}}" url = "{{getAttribute "protocol" $node.Service.Tags "http"}}://{{getBackendAddress $node}}:{{$node.Service.Port}}"
{{$weight := getAttribute "backend.weight" .Service.Tags ""}} {{$weight := getAttribute "backend.weight" $node.Service.Tags ""}}
{{with $weight}} {{with $weight}}
weight = {{$weight}} weight = {{$weight}}
{{end}} {{end}}
@ -25,7 +25,8 @@
{{end}} {{end}}
{{end}} {{end}}
[frontends]{{range .Services}} [frontends]
{{range .Services}}
[frontends.frontend-{{.ServiceName}}] [frontends.frontend-{{.ServiceName}}]
backend = "backend-{{.ServiceName}}" backend = "backend-{{.ServiceName}}"
passHostHeader = {{getAttribute "frontend.passHostHeader" .Attributes "false"}} passHostHeader = {{getAttribute "frontend.passHostHeader" .Attributes "false"}}

16
templates/kubernetes.tmpl Normal file
View file

@ -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}}

View file

@ -1,19 +1,19 @@
{{$frontends := List .Prefix "/frontends/" }} {{$frontends := List .Prefix "/frontends/" }}
{{$backends := List .Prefix "/backends/"}} {{$backends := List .Prefix "/backends/"}}
{{range $backends}} [backends]{{range $backends}}
{{$backend := .}} {{$backend := .}}
{{$servers := List $backend "/servers/" }} {{$servers := List $backend "/servers/" }}
{{$circuitBreaker := Get "" . "/circuitbreaker/" "expression"}} {{$circuitBreaker := Get "" . "/circuitbreaker/" "expression"}}
{{with $circuitBreaker}} {{with $circuitBreaker}}
[backends.{{Last $backend}}.circuitBreaker] [backends."{{Last $backend}}".circuitBreaker]
expression = "{{$circuitBreaker}}" expression = "{{$circuitBreaker}}"
{{end}} {{end}}
{{$loadBalancer := Get "" . "/loadbalancer/" "method"}} {{$loadBalancer := Get "" . "/loadbalancer/" "method"}}
{{with $loadBalancer}} {{with $loadBalancer}}
[backends.{{Last $backend}}.loadBalancer] [backends."{{Last $backend}}".loadBalancer]
method = "{{$loadBalancer}}" method = "{{$loadBalancer}}"
{{end}} {{end}}
@ -21,14 +21,14 @@
{{$maxConnExtractorFunc := Get "" . "/maxconn/" "extractorfunc"}} {{$maxConnExtractorFunc := Get "" . "/maxconn/" "extractorfunc"}}
{{with $maxConnAmt}} {{with $maxConnAmt}}
{{with $maxConnExtractorFunc}} {{with $maxConnExtractorFunc}}
[backends.{{Last $backend}}.maxConn] [backends."{{Last $backend}}".maxConn]
amount = {{$maxConnAmt}} amount = {{$maxConnAmt}}
extractorFunc = "{{$maxConnExtractorFunc}}" extractorFunc = "{{$maxConnExtractorFunc}}"
{{end}} {{end}}
{{end}} {{end}}
{{range $servers}} {{range $servers}}
[backends.{{Last $backend}}.servers.{{Last .}}] [backends."{{Last $backend}}".servers."{{Last .}}"]
url = "{{Get "" . "/url"}}" url = "{{Get "" . "/url"}}"
weight = {{Get "" . "/weight"}} weight = {{Get "" . "/weight"}}
{{end}} {{end}}

View file

@ -323,6 +323,26 @@
# [marathon.TLS] # [marathon.TLS]
# InsecureSkipVerify = true # 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 # Consul KV configuration backend

View file

@ -13,7 +13,7 @@ type Backend struct {
MaxConn *MaxConn `json:"maxConn,omitempty"` MaxConn *MaxConn `json:"maxConn,omitempty"`
} }
// MaxConn holds maximum connection configuraiton // MaxConn holds maximum connection configuration
type MaxConn struct { type MaxConn struct {
Amount int64 `json:"amount,omitempty"` Amount int64 `json:"amount,omitempty"`
ExtractorFunc string `json:"extractorFunc,omitempty"` ExtractorFunc string `json:"extractorFunc,omitempty"`

View file

@ -40,10 +40,10 @@
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li> <li>
<a href="https://github.com/containous/traefik/blob/master/docs/index.md" target="_blank">Documentation</a> <a href="https://docs.traefik.io" target="_blank">Documentation</a>
</li> </li>
<li> <li>
<a href="http://traefik.io" target="_blank"><span class="traefik-blue">traefik.io</span></a> <a href="https://traefik.io" target="_blank"><span class="traefik-blue">traefik.io</span></a>
</li> </li>
</ul> </ul>
</div> </div>