Compare commits
116 Commits
v1.0.0-bet
...
v1.0.0-bet
Author | SHA1 | Date | |
---|---|---|---|
|
c1a12a58eb | ||
|
c3aadab615 | ||
|
26774d2317 | ||
|
61def880db | ||
|
11a6331185 | ||
|
378509cef4 | ||
|
4a1fa03b2d | ||
|
52bff85dda | ||
|
e5b0b34604 | ||
|
0a0063fa27 | ||
|
bf1f6f663a | ||
|
8bac454792 | ||
|
7eaf09b3da | ||
|
378a261e64 | ||
|
53c99f7469 | ||
|
f93e618f67 | ||
|
64b78461f6 | ||
|
2f5c9273ee | ||
|
38371234a2 | ||
|
10cb606578 | ||
|
87caf458df | ||
|
4ff4e4e626 | ||
|
9053f9bb98 | ||
|
b6b72c861f | ||
|
478eed6603 | ||
|
6b76f64b48 | ||
|
53a2787626 | ||
|
cac9927395 | ||
|
9e14619a0b | ||
|
c0dd4c3209 | ||
|
d82e1342fb | ||
|
720912e880 | ||
|
6f47434833 | ||
|
6f13a2c0c7 | ||
|
b7a150bc64 | ||
|
4d22c45b76 | ||
|
2a76a717e6 | ||
|
b3b658a955 | ||
|
c8c0d208be | ||
|
04dd41ac3b | ||
|
10815eca8e | ||
|
06d2f343dd | ||
|
a6c5e85ae7 | ||
|
45d6a326cd | ||
|
0332e32293 | ||
|
2a3a34a80c | ||
|
68da47b59a | ||
|
b1f0f048cd | ||
|
a7b4463f86 | ||
|
ee60adc45a | ||
|
36338b4928 | ||
|
23d3c512c2 | ||
|
4144638be4 | ||
|
f2320ee648 | ||
|
17afa3e672 | ||
|
5b2c355c38 | ||
|
61d54903e3 | ||
|
c1078c4374 | ||
|
4e427b5a9e | ||
|
227ec71db3 | ||
|
d047b8daa1 | ||
|
c2009b71b1 | ||
|
ba8629e2ac | ||
|
6aba453afb | ||
|
a15578a8f6 | ||
|
5c8d9f4eb9 | ||
|
a9e615b3c7 | ||
|
94ad21020c | ||
|
4b76cb4318 | ||
|
fad7ec6b7f | ||
|
82a49a8e89 | ||
|
2bcc5a2ac7 | ||
|
4f044cf2f9 | ||
|
9a407f79ff | ||
|
affec30c64 | ||
|
d050e60da2 | ||
|
866b9835a6 | ||
|
f6564909aa | ||
|
315e8b64b8 | ||
|
f99f634816 | ||
|
5292a5b9d4 | ||
|
cf22d62a74 | ||
|
9363e2ab83 | ||
|
e5ddd92677 | ||
|
04628056af | ||
|
dada86c0b0 | ||
|
92c269c972 | ||
|
6991e3c99b | ||
|
3ee3daee00 | ||
|
85fcff4cf7 | ||
|
30db47d9b6 | ||
|
4d2c85ffdc | ||
|
e36433c23a | ||
|
8486766a60 | ||
|
ef72d355d6 | ||
|
7d013ad5e8 | ||
|
5fcce6567e | ||
|
00af537b0d | ||
|
78449fa62f | ||
|
ab0d648a03 | ||
|
43d2107493 | ||
|
fd8b4a3305 | ||
|
79dc4f9a70 | ||
|
b0fa11b8b8 | ||
|
6e7bb93fd6 | ||
|
e1448eb238 | ||
|
585aeb8f0b | ||
|
563823189a | ||
|
e9bf916a74 | ||
|
bcc5f24c0f | ||
|
9462c2e476 | ||
|
af41c79798 | ||
|
733cbb5304 | ||
|
d5e1d2efd5 | ||
|
bb072a1f8f | ||
|
8737530a7d |
@@ -1,3 +1,5 @@
|
||||
dist/
|
||||
vendor/
|
||||
!dist/traefik
|
||||
site/
|
||||
**/*.test
|
||||
|
5
.gitignore
vendored
@@ -9,4 +9,7 @@ traefik.toml
|
||||
*.test
|
||||
vendor/
|
||||
static/
|
||||
.vscode/
|
||||
.vscode/
|
||||
site/
|
||||
*.log
|
||||
*.exe
|
||||
|
41
.travis.yml
@@ -1,31 +1,28 @@
|
||||
branches:
|
||||
except:
|
||||
- /^v\d\.\d\.\d.*$/
|
||||
|
||||
- /^v\d\.\d\.\d.*$/
|
||||
env:
|
||||
REPO: $TRAVIS_REPO_SLUG
|
||||
VERSION: v1.0.0-beta.$TRAVIS_BUILD_NUMBER
|
||||
|
||||
global:
|
||||
- secure: btt4r13t09gQlHb6gYrvGC2yGCMMHfnp1Mz1RQedc4Mpf/FfT8aE6xmK2a2i9CCvskjrP0t/BFaS4yxIURjnFRn+ugQIEa0pLspB9UJArW/vgOSpIWM9/OQ/fg8z5XuMxN6Md4DL1/iLypMNSageA1x0TRdt89+D1N1dALpg5XRCXLFbC84TLi0gjlFuib9ibPKzEhLT+anCRJ6iZMzeupDSoaCVbAtJMoDvXw4+4AcRZ1+k4MybBLyCib5boaEOt4pTT88mz4Kk0YaMwPVJyg9Qv36VqyUcPS09Yd95LuyVQ4+tZt8Y1ccbIzULsK+sLM3hLCzxlmlpN3dQBlZJiiRtQde0mgGAKyC0P0A1XjuDTywcsa5edB+fTk1Dsewz9xZ9V0NmMz8t+UNZnaSsAPga9i86jULbXUUwMVSzVRc+Xgx02liB/8qI1xYC9FM6ilStt7rn7mF0k3KbiWhcptgeXjO6Lah9FjEKd5w4MXsdUSTi/86rQaLo+kj+XdaTrXCTulKHyRyQEUj+8V1w0oVz7pcGjePHd7y5oU9ByifVQy6sytuFBfRZvugM5bKHo+i0pcWvixrZS42DrzwxZJsspANOvqSe5ifVbvOkfUppQdCBIwptxV5N1b49XPKU3W/w34QJ8xGmKp3TFA7WwVCztriFHjPgiRpB3EG99Bg=
|
||||
- REPO: $TRAVIS_REPO_SLUG
|
||||
- VERSION: v1.0.0-beta.$TRAVIS_BUILD_NUMBER
|
||||
sudo: required
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
- docker
|
||||
install:
|
||||
- sudo service docker stop
|
||||
- sudo curl https://get.docker.com/builds/Linux/x86_64/docker-1.10.1 -o /usr/bin/docker
|
||||
- sudo chmod +x /usr/bin/docker
|
||||
- sudo service docker start
|
||||
|
||||
- sudo service docker stop
|
||||
- sudo curl https://get.docker.com/builds/Linux/x86_64/docker-1.10.1 -o /usr/bin/docker
|
||||
- sudo chmod +x /usr/bin/docker
|
||||
- sudo service docker start
|
||||
- pip install --user mkdocs
|
||||
- pip install --user pymdown-extensions
|
||||
before_script:
|
||||
- make validate
|
||||
- make binary
|
||||
|
||||
- make validate
|
||||
- make binary
|
||||
script:
|
||||
- make test-unit
|
||||
- make test-integration
|
||||
- make crossbinary
|
||||
- make image
|
||||
|
||||
- make test-unit
|
||||
- make test-integration
|
||||
- make crossbinary
|
||||
- make image
|
||||
after_success:
|
||||
- make deploy
|
||||
- make deploy
|
||||
|
BIN
.travis/traefik.id_rsa.enc
Normal file
30
Makefile
@@ -24,36 +24,27 @@ print-%: ; @echo $*=$($*)
|
||||
|
||||
default: binary
|
||||
|
||||
all: generate-webui build
|
||||
all: generate-webui build ## validate all checks, build linux binary, run all tests\ncross non-linux binaries
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh
|
||||
|
||||
binary: generate-webui build
|
||||
binary: generate-webui build ## build the linux binary
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate binary
|
||||
|
||||
crossbinary: generate-webui build
|
||||
crossbinary: generate-webui build ## cross build the non-linux binaries
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate crossbinary
|
||||
|
||||
test: build
|
||||
test: build ## run the unit and integration tests
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate test-unit binary test-integration
|
||||
|
||||
test-unit: build
|
||||
test-unit: build ## run the unit tests
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate test-unit
|
||||
|
||||
test-integration: build
|
||||
test-integration: build ## run the integration tests
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate test-integration
|
||||
|
||||
validate: build
|
||||
validate: build ## validate gofmt, golint and go vet
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt validate-govet validate-golint
|
||||
|
||||
validate-gofmt: build
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt
|
||||
|
||||
validate-govet: build
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-govet
|
||||
|
||||
validate-golint: build
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-golint
|
||||
|
||||
build: dist
|
||||
docker build -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile .
|
||||
|
||||
@@ -63,10 +54,10 @@ build-webui:
|
||||
build-no-cache: dist
|
||||
docker build --no-cache -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile .
|
||||
|
||||
shell: build
|
||||
shell: build ## start a shell inside the build env
|
||||
$(DOCKER_RUN_TRAEFIK) /bin/bash
|
||||
|
||||
image: build
|
||||
image: build ## build a docker traefik image
|
||||
docker build -t $(TRAEFIK_IMAGE) .
|
||||
|
||||
dist:
|
||||
@@ -92,3 +83,6 @@ fmt:
|
||||
|
||||
deploy:
|
||||
./script/deploy.sh
|
||||
|
||||
help: ## this help
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
47
README.md
@@ -1,22 +1,48 @@
|
||||
|
||||
<p align="center">
|
||||
<img src="http://traefik.github.io/traefik.logo.svg" alt="Træfɪk" title="Træfɪk" />
|
||||
<img src="docs/img/traefik.logo.png" alt="Træfɪk" title="Træfɪk" />
|
||||
</p>
|
||||
|
||||
[](https://travis-ci.org/containous/traefik)
|
||||
[](https://docs.traefik.io)
|
||||
[](http://goreportcard.com/report/containous/traefik)
|
||||
[](https://imagelayers.io/?images=traefik)
|
||||
[](https://github.com/containous/traefik/blob/master/LICENSE.md)
|
||||
[](https://traefik.herokuapp.com)
|
||||
[](https://twitter.com/intent/follow?screen_name=traefikproxy)
|
||||
|
||||
|
||||
|
||||
Træfɪk is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
|
||||
It supports several backends ([Docker :whale:](https://www.docker.com/), [Swarm :whale: :whale:](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
|
||||
|
||||
Imagine that you have deployed a bunch of microservices on your infrastructure. You probably used a service registry (like etcd or consul) and/or an orchestrator (swarm, Mesos/Marathon) to manage all these services.
|
||||
If you want your users to access some of your microservices from the Internet, you will have to use a reverse proxy and configure it using virtual hosts or prefix paths:
|
||||
|
||||
- domain `api.domain.com` will point the microservice `api` in your private network
|
||||
- path `domain.com/web` will point the microservice `web` in your private network
|
||||
- domain `backoffice.domain.com` will point the microservices `backoffice` in your private network, load-balancing between your multiple instances
|
||||
|
||||
But a microservices architecture is dynamic... Services are added, removed, killed or upgraded often, eventually several times a day.
|
||||
|
||||
Traditional reverse-proxies are not natively dynamic. You can't change their configuration and hot-reload easily.
|
||||
|
||||
Here enters Træfɪk.
|
||||
|
||||

|
||||
|
||||
Træfɪk can listen to your service registry/orchestrator API, and knows each time a microservice is added, removed, killed or upgraded, and can generate its configuration automatically.
|
||||
Routes to your services will be created instantly.
|
||||
|
||||
Run it and forget it!
|
||||
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- [It's fast](docs/index.md#benchmarks)
|
||||
- [It's fast](http://docs.traefik.io/benchmarks)
|
||||
- No dependency hell, single binary made with go
|
||||
- Rest API
|
||||
- Multiple backends supported: Docker, Mesos/Marathon, Consul, Etcd, and more to come
|
||||
@@ -26,7 +52,7 @@ It supports several backends ([Docker :whale:](https://www.docker.com/), [Swarm
|
||||
- Circuit breakers on backends
|
||||
- Round Robin, rebalancer load-balancers
|
||||
- Rest Metrics
|
||||
- Tiny docker image included [](https://imagelayers.io/?images=containous/traefik:latest)
|
||||
- [Tiny](https://imagelayers.io/?images=traefik) [official](https://hub.docker.com/r/_/traefik/) docker image included
|
||||
- SSL backends support
|
||||
- SSL frontend support (with SNI)
|
||||
- Clean AngularJS Web UI
|
||||
@@ -50,7 +76,7 @@ You can access to a simple HTML frontend of Træfik.
|
||||
|
||||
## Plumbing
|
||||
|
||||
- [Oxy](https://github.com/vulcand/oxy): an awsome proxy library made by Mailgun guys
|
||||
- [Oxy](https://github.com/vulcand/oxy): an awesome proxy library made by Mailgun guys
|
||||
- [Gorilla mux](https://github.com/gorilla/mux): famous request router
|
||||
- [Negroni](https://github.com/codegangsta/negroni): web middlewares made simple
|
||||
- [Manners](https://github.com/mailgun/manners): graceful shutdown of http.Handler servers
|
||||
@@ -67,7 +93,7 @@ You can access to a simple HTML frontend of Træfik.
|
||||
- Use the tiny Docker image:
|
||||
|
||||
```shell
|
||||
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml containous/traefik
|
||||
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik
|
||||
```
|
||||
|
||||
- From sources:
|
||||
@@ -78,7 +104,7 @@ git clone https://github.com/containous/traefik
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find the complete documentation [here](docs/index.md).
|
||||
You can find the complete documentation [here](https://docs.traefik.io).
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -107,8 +133,11 @@ Europe. We provide consulting, development, training and support for the world
|
||||
software products.
|
||||
|
||||
|
||||
|
||||
[](https://aster.is)
|
||||
|
||||
Founded in 2014, Asteris creates next-generation infrastructure software for the modern datacenter. Asteris writes software that makes it easy for companies to implement continuous delivery and realtime data pipelines. We support the HashiCorp stack, along with Kubernetes, Apache Mesos, Spark and Kafka. We're core committers on mantl.io, consul-cli and mesos-consul.
|
||||
.
|
||||
|
||||
## Credits
|
||||
|
||||
Kudos to [Peka](http://peka.byethost11.com/photoblog/) for his awesome work on the logo 
|
32
acme/acme.go
@@ -10,6 +10,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/xenolf/lego/acme"
|
||||
"io/ioutil"
|
||||
fmtlog "log"
|
||||
@@ -142,6 +143,22 @@ type DomainsCertificate struct {
|
||||
tlsCert *tls.Certificate
|
||||
}
|
||||
|
||||
func (dc *DomainsCertificate) needRenew() bool {
|
||||
for _, c := range dc.tlsCert.Certificate {
|
||||
crt, err := x509.ParseCertificate(c)
|
||||
if err != nil {
|
||||
// If there's an error, we assume the cert is broken, and needs update
|
||||
return true
|
||||
}
|
||||
// <= 7 days left, renew certificate
|
||||
if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 7 * time.Hour))) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ACME allows to connect to lets encrypt and retrieve certs
|
||||
type ACME struct {
|
||||
Email string
|
||||
@@ -164,7 +181,7 @@ func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(doma
|
||||
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
|
||||
|
||||
if len(a.StorageFile) == 0 {
|
||||
return errors.New("Empty StorageFile, please provide a filenmae for certs storage")
|
||||
return errors.New("Empty StorageFile, please provide a filename for certs storage")
|
||||
}
|
||||
|
||||
log.Debugf("Generating default certificate...")
|
||||
@@ -226,7 +243,9 @@ func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(doma
|
||||
return err
|
||||
}
|
||||
|
||||
go a.retrieveCertificates(client, account)
|
||||
safe.Go(func() {
|
||||
a.retrieveCertificates(client, account)
|
||||
})
|
||||
|
||||
tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if challengeCert, ok := wrapperChallengeProvider.getCertificate(clientHello.ServerName); ok {
|
||||
@@ -245,7 +264,7 @@ func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(doma
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
go func() {
|
||||
safe.Go(func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
@@ -256,7 +275,7 @@ func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(doma
|
||||
}
|
||||
}
|
||||
|
||||
}()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -289,8 +308,7 @@ func (a *ACME) retrieveCertificates(client *acme.Client, account *Account) {
|
||||
|
||||
func (a *ACME) renewCertificates(client *acme.Client, account *Account) error {
|
||||
for _, certificateResource := range account.DomainsCertificate.Certs {
|
||||
// <= 7 days left, renew certificate
|
||||
if certificateResource.tlsCert.Leaf.NotAfter.Before(time.Now().Add(time.Duration(24 * 7 * time.Hour))) {
|
||||
if certificateResource.needRenew() {
|
||||
log.Debugf("Renewing certificate %+v", certificateResource.Domains)
|
||||
renewedCert, err := client.RenewCertificate(acme.CertificateResource{
|
||||
Domain: certificateResource.Certificate.Domain,
|
||||
@@ -388,7 +406,7 @@ func (a *ACME) saveAccount(Account *Account) error {
|
||||
|
||||
func (a *ACME) getDomainsCertificates(client *acme.Client, domains []string) (*Certificate, error) {
|
||||
log.Debugf("Loading ACME certificates %s...", domains)
|
||||
bundle := false
|
||||
bundle := true
|
||||
certificate, failures := client.ObtainCertificate(domains, bundle, nil)
|
||||
if len(failures) > 0 {
|
||||
log.Error(failures)
|
||||
|
@@ -23,9 +23,9 @@ func (oxylogger *OxyLogger) Warningf(format string, args ...interface{}) {
|
||||
log.Warningf(format, args...)
|
||||
}
|
||||
|
||||
// Errorf logs specified string as Error level in logrus.
|
||||
// Errorf logs specified string as Warningf level in logrus.
|
||||
func (oxylogger *OxyLogger) Errorf(format string, args ...interface{}) {
|
||||
log.Errorf(format, args...)
|
||||
log.Warningf(format, args...)
|
||||
}
|
||||
|
||||
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.6.0-alpine
|
||||
FROM golang:1.6.1-alpine
|
||||
|
||||
RUN apk update && apk add git bash gcc musl-dev \
|
||||
&& go get github.com/Masterminds/glide \
|
||||
|
9
cmd.go
@@ -51,6 +51,7 @@ var arguments = struct {
|
||||
etcd bool
|
||||
etcdTLS bool
|
||||
boltdb bool
|
||||
kubernetes bool
|
||||
}{
|
||||
GlobalConfiguration{
|
||||
EntryPoints: make(EntryPoints),
|
||||
@@ -72,7 +73,8 @@ var arguments = struct {
|
||||
TLS: &provider.KvTLS{},
|
||||
},
|
||||
},
|
||||
Boltdb: &provider.BoltDb{},
|
||||
Boltdb: &provider.BoltDb{},
|
||||
Kubernetes: &provider.Kubernetes{},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
@@ -86,6 +88,7 @@ var arguments = struct {
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -142,6 +145,7 @@ func init() {
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.consulCatalog, "consulCatalog", false, "Enable Consul catalog backend")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Domain, "consulCatalog.domain", "", "Default domain used")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Endpoint, "consulCatalog.endpoint", "127.0.0.1:8500", "Consul server endpoint")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Prefix, "consulCatalog.prefix", "traefik", "Consul catalog tag prefix")
|
||||
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.zookeeper, "zookeeper", false, "Enable Zookeeper backend")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Zookeeper.Watch, "zookeeper.watch", true, "Watch provider")
|
||||
@@ -166,6 +170,9 @@ func init() {
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Endpoint, "boltdb.endpoint", "127.0.0.1:4001", "Boltdb server endpoint")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Prefix, "boltdb.prefix", "/traefik", "Prefix used for KV store")
|
||||
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.kubernetes, "kubernetes", false, "Enable Kubernetes backend")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Kubernetes.Endpoint, "kubernetes.endpoint", "127.0.0.1:8080", "Kubernetes server endpoint")
|
||||
|
||||
_ = viper.BindPFlag("configFile", traefikCmd.PersistentFlags().Lookup("configFile"))
|
||||
_ = viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut"))
|
||||
_ = viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel"))
|
||||
|
@@ -37,6 +37,7 @@ type GlobalConfiguration struct {
|
||||
Etcd *provider.Etcd
|
||||
Zookeeper *provider.Zookepper
|
||||
Boltdb *provider.BoltDb
|
||||
Kubernetes *provider.Kubernetes
|
||||
}
|
||||
|
||||
// DefaultEntryPoints holds default entry points
|
||||
@@ -209,7 +210,11 @@ func LoadConfiguration() *GlobalConfiguration {
|
||||
viper.AddConfigPath("$HOME/.traefik/") // call multiple times to add many search paths
|
||||
viper.AddConfigPath(".") // optionally look for config in the working directory
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
fmtlog.Fatalf("Error reading file: %s", err)
|
||||
if len(viper.ConfigFileUsed()) > 0 {
|
||||
fmtlog.Printf("Error reading configuration file: %s", err)
|
||||
} else {
|
||||
fmtlog.Printf("No configuration file found")
|
||||
}
|
||||
}
|
||||
|
||||
if len(arguments.EntryPoints) > 0 {
|
||||
@@ -254,6 +259,9 @@ func LoadConfiguration() *GlobalConfiguration {
|
||||
if arguments.boltdb {
|
||||
viper.Set("boltdb", arguments.Boltdb)
|
||||
}
|
||||
if arguments.kubernetes {
|
||||
viper.Set("kubernetes", arguments.Kubernetes)
|
||||
}
|
||||
if err := unmarshal(&configuration); err != nil {
|
||||
|
||||
fmtlog.Fatalf("Error reading file: %s", err)
|
||||
|
1
docs/CNAME
Normal file
@@ -0,0 +1 @@
|
||||
docs.traefik.io
|
202
docs/basics.md
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
# Concepts
|
||||
|
||||
Let's take our example from the [overview](https://docs.traefik.io/#overview) again:
|
||||
|
||||
|
||||
> Imagine that you have deployed a bunch of microservices on your infrastructure. You probably used a service registry (like etcd or consul) and/or an orchestrator (swarm, Mesos/Marathon) to manage all these services.
|
||||
> If you want your users to access some of your microservices from the Internet, you will have to use a reverse proxy and configure it using virtual hosts or prefix paths:
|
||||
|
||||
> - domain `api.domain.com` will point the microservice `api` in your private network
|
||||
> - path `domain.com/web` will point the microservice `web` in your private network
|
||||
> - domain `backoffice.domain.com` will point the microservices `backoffice` in your private network, load-balancing between your multiple instances
|
||||
|
||||
> 
|
||||
|
||||
Let's zoom on Træfɪk and have an overview of its internal architecture:
|
||||
|
||||
|
||||

|
||||
|
||||
- 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 forwarded to a matching [frontend](#frontends). A frontend defines routes from [entrypoints](#entrypoints) to [backends](#backends).
|
||||
Routes are created using requests fields (`Host`, `Path`, `Headers`...) and can match or not a request.
|
||||
- The [frontend](#frontends) will then send the request to a [backend](#backends). A backend can be composed by one or more [servers](#servers), and by a load-balancing strategy.
|
||||
- Finally, the [server](#servers) will forward the request to the corresponding microservice in the private network.
|
||||
|
||||
## Entrypoints
|
||||
|
||||
Entrypoints are the network entry points into Træfɪk.
|
||||
They can be defined using:
|
||||
|
||||
- a port (80, 443...)
|
||||
- SSL (Certificates. Keys...)
|
||||
- redirection to another entrypoint (redirect `HTTP` to `HTTPS`)
|
||||
|
||||
Here is an example of entrypoints definition:
|
||||
|
||||
```toml
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
[entryPoints.http.redirect]
|
||||
entryPoint = "https"
|
||||
[entryPoints.https]
|
||||
address = ":443"
|
||||
[entryPoints.https.tls]
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
certFile = "tests/traefik.crt"
|
||||
keyFile = "tests/traefik.key"
|
||||
```
|
||||
|
||||
- Two entrypoints are defined `http` and `https`.
|
||||
- `http` listens on port `80` et `https` on port `443`.
|
||||
- We enable SSL en `https` by giving a certificate and a key.
|
||||
- We also redirect all the traffic from entrypoint `http` to `https`.
|
||||
|
||||
## Frontends
|
||||
|
||||
A frontend is a set of rules that forwards the incoming traffic from an entrypoint to a backend.
|
||||
Frontends can be defined using the following rules:
|
||||
|
||||
- `Headers: Content-Type, application/json`: Headers adds a matcher for request header values. It accepts a sequence of key/value pairs to be matched.
|
||||
- `HeadersRegexp: Content-Type, application/(text|json)`: Regular expressions can be used with headers as well. It accepts a sequence of key/value pairs, where the value has regex support.
|
||||
- `Host: traefik.io, www.traefik.io`: Match request host with given host list.
|
||||
- `HostRegexp: traefik.io, {subdomain:[a-z]+}.traefik.io`: Adds a matcher for the URL hosts. It accepts templates with zero or more URL variables enclosed by `{}`. Variables can define an optional regexp pattern to be matched.
|
||||
- `Method: GET, POST, PUT`: Method adds a matcher for HTTP methods. It accepts a sequence of one or more methods to be matched.
|
||||
- `Path: /products/, /articles/{category}/{id:[0-9]+}`: Path adds a matcher for the URL paths. It accepts templates with zero or more URL variables enclosed by `{}`.
|
||||
- `PathStrip`: Same as `Path` but strip the given prefix from the request URL's Path.
|
||||
- `PathPrefix`: PathPrefix adds a matcher for the URL path prefixes. This matches if the given template is a prefix of the full URL path.
|
||||
- `PathPrefixStrip`: Same as `PathPrefix` but strip the given prefix from the request URL's Path.
|
||||
|
||||
You can optionally enable `passHostHeader` to forward client `Host` header to the backend.
|
||||
|
||||
Here is an example of frontends definition:
|
||||
|
||||
```toml
|
||||
[frontends]
|
||||
[frontends.frontend1]
|
||||
backend = "backend2"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host: test.localhost, test2.localhost"
|
||||
[frontends.frontend2]
|
||||
backend = "backend1"
|
||||
passHostHeader = true
|
||||
entrypoints = ["https"] # overrides defaultEntryPoints
|
||||
[frontends.frontend2.routes.test_1]
|
||||
rule = "Host: localhost, {subdomain:[a-z]+}.localhost"
|
||||
[frontends.frontend3]
|
||||
backend = "backend2"
|
||||
rule = "Path:/test"
|
||||
```
|
||||
|
||||
- Three frontends are defined: `frontend1`, `frontend2` and `frontend3`
|
||||
- `frontend1` will forward the traffic to the `backend2` if the rule `Host: test.localhost, test2.localhost` is matched
|
||||
- `frontend2` will forward the traffic to the `backend1` if the rule `Host: localhost, {subdomain:[a-z]+}.localhost` is matched (forwarding client `Host` header to the backend)
|
||||
- `frontend3` will forward the traffic to the `backend2` if the rule `Path:/test` is matched
|
||||
|
||||
## Backends
|
||||
|
||||
A backend is responsible to load-balance the traffic coming from one or more frontends to a set of http servers.
|
||||
Various methods of load-balancing is supported:
|
||||
|
||||
- `wrr`: Weighted Round Robin
|
||||
- `drr`: Dynamic Round Robin: increases weights on servers that perform better than others. It also rolls back to original weights if the servers have changed.
|
||||
|
||||
A circuit breaker can also be applied to a backend, preventing high loads on failing servers.
|
||||
Initial state is Standby. CB observes the statistics and does not modify the request.
|
||||
In case if condition matches, CB enters Tripped state, where it responds with predefines code or redirects to another frontend.
|
||||
Once Tripped timer expires, CB enters Recovering state and resets all stats.
|
||||
In case if the condition does not match and recovery timer expires, CB enters Standby state.
|
||||
|
||||
It can be configured using:
|
||||
|
||||
- Methods: `LatencyAtQuantileMS`, `NetworkErrorRatio`, `ResponseCodeRatio`
|
||||
- Operators: `AND`, `OR`, `EQ`, `NEQ`, `LT`, `LE`, `GT`, `GE`
|
||||
|
||||
For example:
|
||||
|
||||
- `NetworkErrorRatio() > 0.5`: watch error ratio over 10 second sliding window for a frontend
|
||||
- `LatencyAtQuantileMS(50.0) > 50`: watch latency at quantile in milliseconds.
|
||||
- `ResponseCodeRatio(500, 600, 0, 600) > 0.5`: ratio of response codes in range [500-600) to [0-600)
|
||||
|
||||
To proactively prevent backends from being overwhelmed with high load, a maximum connection limit can
|
||||
also be applied to each backend.
|
||||
|
||||
Maximum connections can be configured by specifying an integer value for `maxconn.amount` and
|
||||
`maxconn.extractorfunc` which is a strategy used to determine how to categorize requests in order to
|
||||
evaluate the maximum connections.
|
||||
|
||||
For example:
|
||||
```toml
|
||||
[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.maxconn]
|
||||
amount = 10
|
||||
extractorfunc = "request.host"
|
||||
```
|
||||
|
||||
- `backend1` will return `HTTP code 429 Too Many Requests` if there are already 10 requests in progress for the same Host header.
|
||||
- Another possible value for `extractorfunc` is `client.ip` which will categorize requests based on client source ip.
|
||||
- Lastly `extractorfunc` can take the value of `request.header.ANY_HEADER` which will categorize requests based on `ANY_HEADER` that you provide.
|
||||
|
||||
## Servers
|
||||
|
||||
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:
|
||||
|
||||
```toml
|
||||
[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.circuitbreaker]
|
||||
expression = "NetworkErrorRatio() > 0.5"
|
||||
[backends.backend1.servers.server1]
|
||||
url = "http://172.17.0.2:80"
|
||||
weight = 10
|
||||
[backends.backend1.servers.server2]
|
||||
url = "http://172.17.0.3:80"
|
||||
weight = 1
|
||||
[backends.backend2]
|
||||
[backends.backend2.LoadBalancer]
|
||||
method = "drr"
|
||||
[backends.backend2.servers.server1]
|
||||
url = "http://172.17.0.4:80"
|
||||
weight = 1
|
||||
[backends.backend2.servers.server2]
|
||||
url = "http://172.17.0.5:80"
|
||||
weight = 2
|
||||
```
|
||||
|
||||
- Two backends are defined: `backend1` and `backend2`
|
||||
- `backend1` will forward the traffic to two servers: `http://172.17.0.2:80"` with weight `10` and `http://172.17.0.3:80` with weight `1` using default `wrr` load-balancing strategy.
|
||||
- `backend2` will forward the traffic to two servers: `http://172.17.0.4:80"` with weight `1` and `http://172.17.0.5:80` with weight `2` using `drr` load-balancing strategy.
|
||||
- a circuit breaker is added on `backend1` using the expression `NetworkErrorRatio() > 0.5`: watch error ratio over 10 second sliding window
|
||||
|
||||
# Launch
|
||||
|
||||
Træfɪk can be configured using a TOML file configuration, arguments, or both.
|
||||
By default, Træfɪk will try to find a `traefik.toml` in the following places:
|
||||
|
||||
- `/etc/traefik/`
|
||||
- `$HOME/.traefik/`
|
||||
- `.` *the working directory*
|
||||
|
||||
You can override this by setting a `configFile` argument:
|
||||
|
||||
```bash
|
||||
$ traefik --configFile=foo/bar/myconfigfile.toml
|
||||
```
|
||||
|
||||
Træfɪk uses the following precedence order. Each item takes precedence over the item below it:
|
||||
|
||||
- arguments
|
||||
- configuration file
|
||||
- default
|
||||
|
||||
It means that arguments overrides configuration file.
|
||||
Each argument is described in the help section:
|
||||
|
||||
```bash
|
||||
$ traefik --help
|
||||
```
|
213
docs/benchmarks.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# Benchmarks
|
||||
|
||||
## Configuration
|
||||
|
||||
I would like to thanks [vincentbernat](https://github.com/vincentbernat) from [exoscale.ch](https://www.exoscale.ch) who kindly provided the infrastructure needed for the benchmarks.
|
||||
|
||||
I used 4 VMs for the tests with the following configuration:
|
||||
|
||||
- 32 GB RAM
|
||||
- 8 CPU Cores
|
||||
- 10 GB SSD
|
||||
- Ubuntu 14.04 LTS 64-bit
|
||||
|
||||
## Setup
|
||||
|
||||
1. One VM used to launch the benchmarking tool [wrk](https://github.com/wg/wrk)
|
||||
2. One VM for traefik (v1.0.0-beta.416) / nginx (v1.4.6)
|
||||
3. Two VMs for 2 backend servers in go [whoami](https://github.com/emilevauge/whoamI/)
|
||||
|
||||
Each VM has been tuned using the following limits:
|
||||
|
||||
```bash
|
||||
sysctl -w fs.file-max="9999999"
|
||||
sysctl -w fs.nr_open="9999999"
|
||||
sysctl -w net.core.netdev_max_backlog="4096"
|
||||
sysctl -w net.core.rmem_max="16777216"
|
||||
sysctl -w net.core.somaxconn="65535"
|
||||
sysctl -w net.core.wmem_max="16777216"
|
||||
sysctl -w net.ipv4.ip_local_port_range="1025 65535"
|
||||
sysctl -w net.ipv4.tcp_fin_timeout="30"
|
||||
sysctl -w net.ipv4.tcp_keepalive_time="30"
|
||||
sysctl -w net.ipv4.tcp_max_syn_backlog="20480"
|
||||
sysctl -w net.ipv4.tcp_max_tw_buckets="400000"
|
||||
sysctl -w net.ipv4.tcp_no_metrics_save="1"
|
||||
sysctl -w net.ipv4.tcp_syn_retries="2"
|
||||
sysctl -w net.ipv4.tcp_synack_retries="2"
|
||||
sysctl -w net.ipv4.tcp_tw_recycle="1"
|
||||
sysctl -w net.ipv4.tcp_tw_reuse="1"
|
||||
sysctl -w vm.min_free_kbytes="65536"
|
||||
sysctl -w vm.overcommit_memory="1"
|
||||
ulimit -n 9999999
|
||||
```
|
||||
|
||||
### Nginx
|
||||
|
||||
Here is the config Nginx file use `/etc/nginx/nginx.conf`:
|
||||
|
||||
```
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
worker_rlimit_nofile 200000;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 10000;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 300;
|
||||
keepalive_requests 10000;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
open_file_cache max=200000 inactive=300s;
|
||||
open_file_cache_valid 300s;
|
||||
open_file_cache_min_uses 2;
|
||||
open_file_cache_errors on;
|
||||
|
||||
server_tokens off;
|
||||
dav_methods off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
access_log /var/log/nginx/access.log combined;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
gzip off;
|
||||
gzip_vary off;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*.conf;
|
||||
}
|
||||
```
|
||||
|
||||
Here is the Nginx vhost file used:
|
||||
|
||||
```
|
||||
upstream whoami {
|
||||
server IP-whoami1:80;
|
||||
server IP-whoami2:80;
|
||||
keepalive 300;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8001;
|
||||
server_name test.traefik;
|
||||
access_log off;
|
||||
error_log /dev/null crit;
|
||||
if ($host != "test.traefik") {
|
||||
return 404;
|
||||
}
|
||||
location / {
|
||||
proxy_pass http://whoami;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Traefik
|
||||
|
||||
Here is the `traefik.toml` file used:
|
||||
|
||||
```
|
||||
MaxIdleConnsPerHost = 100000
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
[file]
|
||||
[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.servers.server1]
|
||||
url = "http://IP-whoami1:80"
|
||||
weight = 1
|
||||
[backends.backend1.servers.server2]
|
||||
url = "http://IP-whoami2:80"
|
||||
weight = 1
|
||||
|
||||
[frontends]
|
||||
[frontends.frontend1]
|
||||
backend = "backend1"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host: test.traefik"
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
### whoami:
|
||||
```
|
||||
wrk -t8 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-whoami:80/bench
|
||||
Running 1m test @ http://IP-whoami:80/bench
|
||||
20 threads and 1000 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 70.28ms 134.72ms 1.91s 89.94%
|
||||
Req/Sec 2.92k 742.42 8.78k 68.80%
|
||||
Latency Distribution
|
||||
50% 10.63ms
|
||||
75% 75.64ms
|
||||
90% 205.65ms
|
||||
99% 668.28ms
|
||||
3476705 requests in 1.00m, 384.61MB read
|
||||
Socket errors: connect 0, read 0, write 0, timeout 103
|
||||
Requests/sec: 57894.35
|
||||
Transfer/sec: 6.40MB
|
||||
```
|
||||
|
||||
### nginx:
|
||||
```
|
||||
wrk -t20 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-nginx:8001/bench
|
||||
Running 1m test @ http://IP-nginx:8001/bench
|
||||
20 threads and 1000 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 101.25ms 180.09ms 1.99s 89.34%
|
||||
Req/Sec 1.69k 567.69 9.39k 72.62%
|
||||
Latency Distribution
|
||||
50% 15.46ms
|
||||
75% 129.11ms
|
||||
90% 302.44ms
|
||||
99% 846.59ms
|
||||
2018427 requests in 1.00m, 298.36MB read
|
||||
Socket errors: connect 0, read 0, write 0, timeout 90
|
||||
Requests/sec: 33591.67
|
||||
Transfer/sec: 4.97MB
|
||||
```
|
||||
|
||||
### traefik:
|
||||
```
|
||||
wrk -t8 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-traefik:8000/bench
|
||||
Running 1m test @ http://IP-traefik:8000/bench
|
||||
20 threads and 1000 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 91.72ms 150.43ms 2.00s 90.50%
|
||||
Req/Sec 1.43k 266.37 2.97k 69.77%
|
||||
Latency Distribution
|
||||
50% 19.74ms
|
||||
75% 121.98ms
|
||||
90% 237.39ms
|
||||
99% 687.49ms
|
||||
1705073 requests in 1.00m, 188.63MB read
|
||||
Socket errors: connect 0, read 0, write 0, timeout 7
|
||||
Requests/sec: 28392.44
|
||||
Transfer/sec: 3.14MB
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Traefik is obviously slower than Nginx, but not so much: Traefik can serve 28392 requests/sec and Nginx 33591 requests/sec which gives a ratio of 85%.
|
||||
Not bad for young project :) !
|
||||
|
||||
Some areas of possible improvements:
|
||||
|
||||
- Use [GO_REUSEPORT](https://github.com/kavu/go_reuseport) listener
|
||||
- Run a separate server instance per CPU core with `GOMAXPROCS=1` (it appears during benchmarks that there is a lot more context switches with traefik than with nginx)
|
||||
|
61
docs/css/traefik.css
Normal file
@@ -0,0 +1,61 @@
|
||||
a {
|
||||
color: #37ABC8;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover, a:focus {
|
||||
color: #25606F;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1, h2, h3, H4 {
|
||||
color: #37ABC8;
|
||||
}
|
||||
|
||||
.navbar-default {
|
||||
background-color: #37ABC8;
|
||||
border-color: #25606F;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>.active>a, .navbar-default .navbar-nav>.active>a:hover, .navbar-default .navbar-nav>.active>a:focus {
|
||||
color: #fff;
|
||||
background-color: #25606F;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>li>a:hover, .navbar-default .navbar-nav>li>a:focus {
|
||||
color: #fff;
|
||||
background-color: #25606F;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-toggle {
|
||||
border-color: #25606F;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus .navbar-toggle {
|
||||
background-color: #25606F;
|
||||
}
|
||||
.navbar-default .navbar-collapse, .navbar-default .navbar-form {
|
||||
border-color: #25606F;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>.open>a, .navbar-default .navbar-nav>.open>a:hover, .navbar-default .navbar-nav>.open>a:focus {
|
||||
color: #fff;
|
||||
background-color: #25606F;
|
||||
}
|
||||
|
||||
.dropdown-menu>li>a:hover, .dropdown-menu>li>a:focus {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background-color: #25606F;
|
||||
}
|
||||
|
||||
.dropdown-menu>.active>a, .dropdown-menu>.active>a:hover, .dropdown-menu>.active>a:focus {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
background-color: #25606F;
|
||||
outline: 0;
|
||||
}
|
BIN
docs/img/architecture.png
Normal file
After Width: | Height: | Size: 354 KiB |
2407
docs/img/architecture.svg
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
docs/img/internal.png
Normal file
After Width: | Height: | Size: 323 KiB |
172
docs/img/letsencrypt-logo-horizontal.svg
Normal file
@@ -0,0 +1,172 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="337.37802"
|
||||
height="107.921"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.4 r9939"
|
||||
sodipodi:docname="letsencrypt-logo-horizontal.svg">
|
||||
<metadata
|
||||
id="metadata37">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs35" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="640"
|
||||
inkscape:window-height="480"
|
||||
id="namedview33"
|
||||
showgrid="false"
|
||||
fit-margin-bottom="30"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
inkscape:zoom="0.72861357"
|
||||
inkscape:cx="168.57"
|
||||
inkscape:cy="69.027001"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="30"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<g
|
||||
id="g4"
|
||||
transform="translate(-0.930001,-1.606)">
|
||||
<title
|
||||
id="title6">Layer 1</title>
|
||||
<g
|
||||
id="svg_1">
|
||||
<g
|
||||
id="svg_2">
|
||||
<g
|
||||
id="svg_3">
|
||||
<path
|
||||
id="svg_4"
|
||||
d="m 76.621002,68.878998 0,-31.406998 7.629997,0 0,24.796997 12.153999,0 0,6.609001 -19.783997,0 0,9.99e-4 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
<path
|
||||
id="svg_5"
|
||||
d="m 121.547,58.098999 c 0,0.295998 0,0.592003 0,0.888 0,0.295997 -0.015,0.576004 -0.044,0.843002 l -16.01301,0 c 0.059,0.620995 0.244,1.182999 0.555,1.685997 0.311,0.502998 0.71,0.938004 1.197,1.308998 0.488,0.370003 1.035,0.658005 1.642,0.864006 0.605,0.208 1.234,0.310997 1.885,0.310997 1.153,0 2.13,-0.213997 2.928,-0.642998 0.799,-0.429001 1.449,-0.983002 1.952,-1.664001 l 5.05699,3.194 c -1.03498,1.507996 -2.40199,2.668999 -4.10299,3.482002 -1.701,0.811996 -3.676,1.219994 -5.922,1.219994 -1.657,0 -3.224,-0.259995 -4.702,-0.775993 -1.479,-0.518005 -2.772,-1.271004 -3.882,-2.263 -1.108,-0.990005 -1.981,-2.210007 -2.616996,-3.659004 -0.635994,-1.448997 -0.953003,-3.104996 -0.953003,-4.969002 0,-1.802994 0.309998,-3.437996 0.931,-4.900997 0.620999,-1.463001 1.463999,-2.706001 2.528999,-3.726002 1.064,-1.021 2.32,-1.811996 3.771,-2.373997 1.448,-0.561001 3.016,-0.843002 4.701,-0.843002 1.626,0 3.12,0.274002 4.48,0.820999 1.36,0.546997 2.528,1.338001 3.505,2.373001 0.976,1.035 1.73599,2.292 2.284,3.771 0.546,1.478001 0.819,3.165001 0.819,5.056 z m -6.698,-2.794998 c 0,-1.153 -0.362,-2.144001 -1.087,-2.972 -0.725,-0.827 -1.812,-1.242001 -3.26,-1.242001 -0.71,0 -1.36,0.111 -1.952,0.333 -0.59199,0.222 -1.108,0.525002 -1.553,0.909 -0.443,0.384998 -0.798,0.835999 -1.064,1.354 -0.266,0.517998 -0.414,1.057999 -0.443,1.618 l 9.359,0 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
<path
|
||||
id="svg_6"
|
||||
d="m 133.168,52.200001 0,8.461002 c 0,1.038994 0.2,1.816994 0.60001,2.337997 0.39799,0.519997 1.11499,0.778 2.151,0.778 0.35399,0 0.73098,-0.028 1.13099,-0.089 0.39901,-0.05901 0.73101,-0.147003 0.998,-0.266006 l 0.089,5.323006 c -0.50299,0.176994 -1.13899,0.332001 -1.90699,0.465996 -0.76999,0.133003 -1.538,0.199005 -2.307,0.199005 -1.479,0 -2.722,-0.186005 -3.727,-0.556007 C 129.19,68.484002 128.384,67.949998 127.77901,67.252 127.172,66.556001 126.73599,65.725999 126.47,64.762002 126.203,63.799005 126.071,62.724 126.071,61.538003 l 0,-9.338001 -3.549,0 0,-5.412003 3.504,0 0,-5.810997 7.142,0 0,5.810997 5.19,0 0,5.412003 -5.19,0 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
<path
|
||||
id="svg_7"
|
||||
d="m 161.91299,53.307999 c -0.59201,-0.560997 -1.28601,-1.034 -2.085,-1.418999 -0.79801,-0.383999 -1.64099,-0.577 -2.528,-0.577 -0.681,0 -1.30899,0.133999 -1.885,0.398998 -0.57699,0.267002 -0.865,0.726002 -0.865,1.375 0,0.621002 0.317,1.064003 0.953,1.331001 0.636,0.266998 1.664,0.562 3.08299,0.887001 0.82801,0.177998 1.664,0.43 2.50701,0.754997 0.843,0.324997 1.604,0.754005 2.28399,1.286003 0.68001,0.531998 1.22701,1.182999 1.64202,1.951996 0.41299,0.769005 0.62098,1.686005 0.62098,2.75 0,1.391006 -0.28099,2.565002 -0.84298,3.526001 -0.56201,0.960999 -1.29401,1.737 -2.19602,2.329002 -0.902,0.592002 -1.91499,1.019997 -3.03799,1.286003 -1.12399,0.266998 -2.248,0.398994 -3.371,0.398994 -1.80499,0 -3.571,-0.287994 -5.302,-0.864998 C 149.161,68.146002 147.719,67.294996 146.566,66.170995 l 4.08099,-4.303001 c 0.649,0.710007 1.448,1.302002 2.395,1.774002 0.946,0.473999 1.952,0.709999 3.017,0.709999 0.592,0 1.176,-0.140999 1.752,-0.421997 0.577,-0.279999 0.86501,-0.776001 0.86501,-1.485001 0,-0.681 -0.35401,-1.182999 -1.06401,-1.509003 -0.71,-0.324997 -1.818,-0.664993 -3.327,-1.020996 -0.769,-0.177002 -1.53799,-0.413002 -2.30699,-0.709 -0.77001,-0.295998 -1.457,-0.694 -2.06202,-1.197998 -0.60598,-0.502007 -1.10199,-1.123001 -1.48599,-1.863007 -0.384,-0.737995 -0.576,-1.625996 -0.576,-2.660995 0,-1.331001 0.28,-2.462002 0.843,-3.394001 0.562,-0.931999 1.286,-1.692001 2.174,-2.284 0.88701,-0.591999 1.87001,-1.027 2.949,-1.308998 1.079,-0.281998 2.151,-0.422001 3.217,-0.422001 1.655,0 3.274,0.259998 4.856,0.776001 1.582,0.517998 2.921,1.293999 4.015,2.328999 l -3.995,4.127998 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
<path
|
||||
id="svg_8"
|
||||
d="m 179.56799,68.878998 0,-31.406998 21.114,0 0,6.388 -13.795,0 0,5.944 13.041,0 0,6.077 -13.041,0 0,6.521 14.594,0 0,6.476997 -21.913,0 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
<path
|
||||
id="svg_9"
|
||||
d="m 220.675,68.878998 0,-12.065994 c 0,-0.621002 -0.053,-1.212002 -0.155,-1.774002 -0.104,-0.562 -0.274,-1.057003 -0.511,-1.486 -0.237,-0.428001 -0.569,-0.769001 -0.998,-1.021 -0.429,-0.25 -0.96899,-0.377003 -1.619,-0.377003 -0.65001,0 -1.22,0.127003 -1.70799,0.377003 -0.487,0.251999 -0.89501,0.599998 -1.22001,1.042999 -0.32499,0.443001 -0.569,0.953999 -0.731,1.529999 -0.16299,0.577 -0.244,1.175999 -0.244,1.797001 l 0,11.976997 -7.319,0 0,-22.091 7.05301,0 0,3.061001 0.089,0 c 0.26699,-0.473 0.613,-0.938 1.043,-1.396 0.428,-0.459 0.932,-0.850998 1.50801,-1.175999 0.57699,-0.325001 1.20498,-0.591999 1.88598,-0.799 0.68001,-0.206001 1.40401,-0.311001 2.17301,-0.311001 1.479,0 2.735,0.266998 3.77099,0.799 1.036,0.532002 1.87001,1.220001 2.50701,2.062 0.636,0.842999 1.09401,1.812 1.375,2.904999 0.28,1.095001 0.421,2.189003 0.421,3.283001 l 0,13.661999 -7.321,0 0,9.99e-4 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
<path
|
||||
id="svg_10"
|
||||
d="m 246.71301,53.929001 c -0.41501,-0.532001 -0.977,-0.959999 -1.686,-1.285999 -0.70999,-0.325001 -1.43601,-0.488003 -2.174,-0.488003 -0.77,0 -1.464,0.155003 -2.085,0.466 -0.62101,0.310997 -1.153,0.726002 -1.59701,1.242001 -0.44299,0.518002 -0.79199,1.117001 -1.04299,1.797001 -0.251,0.681004 -0.377,1.404003 -0.377,2.174 0,0.768997 0.11799,1.493004 0.35499,2.173004 0.23601,0.681 0.58301,1.279999 1.04201,1.796997 0.45799,0.517998 1.005,0.924995 1.642,1.220001 0.636,0.295998 1.35299,0.443001 2.151,0.443001 0.73801,0 1.47099,-0.139999 2.19501,-0.421005 0.72401,-0.281006 1.30899,-0.687996 1.75198,-1.220001 l 4.03702,4.924004 c -0.91703,0.887001 -2.10102,1.582001 -3.54901,2.084999 -1.44899,0.501999 -2.987,0.753998 -4.61299,0.753998 -1.74501,0 -3.37401,-0.266998 -4.88701,-0.798996 -1.512,-0.531998 -2.82601,-1.308998 -3.941,-2.329002 -1.11599,-1.019997 -1.99299,-2.253998 -2.63299,-3.702995 -0.64,-1.448997 -0.959,-3.090004 -0.959,-4.924004 0,-1.804001 0.31898,-3.431 0.959,-4.880001 0.64,-1.447998 1.51699,-2.683998 2.63299,-3.703999 1.11499,-1.021 2.43,-1.804001 3.941,-2.351002 1.513,-0.546997 3.127,-0.820999 4.843,-0.820999 0.798,0 1.589,0.074 2.373,0.223 0.783,0.147003 1.53699,0.348 2.26199,0.599003 0.72501,0.251003 1.39002,0.562 1.996,0.931999 0.60599,0.369999 1.13202,0.776001 1.57502,1.219997 l -4.21201,4.877003 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
<path
|
||||
id="svg_11"
|
||||
d="m 268.03201,52.776001 c -0.32599,-0.089 -0.64401,-0.146999 -0.95401,-0.177002 -0.30999,-0.03 -0.61398,-0.045 -0.90899,-0.045 -0.97599,0 -1.797,0.177998 -2.46201,0.530998 -0.66498,0.354 -1.19699,0.781002 -1.59698,1.283001 -0.39902,0.500999 -0.68802,1.047001 -0.86503,1.636997 -0.177,0.589996 -0.26599,1.105003 -0.26599,1.548004 l 0,11.324997 -7.27499,0 0,-22.063999 7.009,0 0,3.194 0.089,0 c 0.56201,-1.132 1.35901,-2.055 2.396,-2.77 1.03402,-0.715 2.23202,-1.071999 3.59302,-1.071999 0.29498,0 0.58398,0.016 0.86499,0.045 0.27999,0.029 0.51001,0.074 0.68801,0.133003 L 268.03201,52.776 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
<path
|
||||
id="svg_12"
|
||||
d="m 285.12201,72.206001 c -0.44299,1.153 -0.939,2.181 -1.48599,3.083 -0.547,0.901001 -1.19702,1.669998 -1.95102,2.306999 -0.754,0.636002 -1.642,1.114998 -2.66199,1.441002 -1.01999,0.324997 -2.22601,0.487999 -3.61499,0.487999 -0.681,0 -1.38299,-0.045 -2.10602,-0.134003 -0.72598,-0.089 -1.354,-0.207001 -1.88598,-0.353996 L 272.215,72.916 c 0.354,0.116997 0.746,0.213997 1.17602,0.288002 0.42798,0.073 0.81998,0.110001 1.17499,0.110001 1.12399,0 1.93701,-0.259003 2.44,-0.776001 0.50199,-0.518005 0.931,-1.249001 1.28601,-2.195 l 0.70999,-1.818001 -9.22699,-21.736 8.073,0 4.92398,14.195 0.133,0 4.392,-14.195 7.71802,0 -9.89301,25.417 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
<path
|
||||
id="svg_13"
|
||||
d="m 321.496,57.745003 c 0,1.537994 -0.237,3.016998 -0.70999,4.435997 -0.474,1.419998 -1.16101,2.668999 -2.06201,3.748001 -0.90201,1.080002 -2.004,1.945 -3.30499,2.596001 -1.30201,0.649002 -2.78,0.975998 -4.43702,0.975998 -1.35998,0 -2.64599,-0.273003 -3.85901,-0.82 -1.21301,-0.546997 -2.15799,-1.293999 -2.83898,-2.239998 l -0.088,0 0,13.085999 -7.27502,0 0,-32.739002 6.92001,0 0,2.706001 0.133,0 c 0.681,-0.887001 1.61899,-1.662998 2.81698,-2.328999 C 307.98801,46.5 309.39999,46.167 311.02701,46.167 c 1.59698,0 3.04498,0.311001 4.34698,0.931999 1.301,0.621002 2.40201,1.464001 3.305,2.528 0.90298,1.063999 1.59701,2.299999 2.08502,3.704002 0.488,1.404999 0.73199,2.876999 0.73199,4.414001 z m -7.05301,0 c 0,-0.709999 -0.11001,-1.403999 -0.332,-2.085003 -0.22201,-0.68 -0.548,-1.278999 -0.97699,-1.797001 -0.42901,-0.516998 -0.96902,-0.938 -1.61902,-1.264 -0.64999,-0.326 -1.40399,-0.487999 -2.26199,-0.487999 -0.828,0 -1.56799,0.162998 -2.21799,0.487999 -0.651,0.325001 -1.20602,0.754002 -1.664,1.285999 -0.45901,0.532001 -0.81302,1.139 -1.06402,1.818001 -0.25199,0.681004 -0.37699,1.375004 -0.37699,2.085003 0,0.709999 0.125,1.404999 0.37699,2.084999 0.251,0.681 0.60501,1.285995 1.06402,1.818001 0.45798,0.531998 1.013,0.961998 1.664,1.286995 0.64899,0.325005 1.38999,0.487 2.21799,0.487 0.85699,0 1.61099,-0.161995 2.26199,-0.487 0.651,-0.325005 1.19001,-0.754997 1.61902,-1.286995 0.42902,-0.531998 0.75498,-1.146004 0.97699,-1.841003 0.22101,-0.693001 0.332,-1.394997 0.332,-2.104996 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
<path
|
||||
id="svg_14"
|
||||
d="m 333.11801,52.200001 0,8.461002 c 0,1.038994 0.20001,1.816994 0.60001,2.337997 0.39798,0.519997 1.11499,0.778 2.151,0.778 0.354,0 0.73099,-0.028 1.13098,-0.089 0.39902,-0.05901 0.73102,-0.147003 0.99802,-0.266006 l 0.089,5.323006 c -0.50299,0.176994 -1.139,0.332001 -1.90698,0.465996 -0.77002,0.133003 -1.53802,0.199005 -2.307,0.199005 -1.47901,0 -2.72202,-0.186005 -3.72702,-0.556007 -1.00599,-0.369995 -1.81199,-0.903999 -2.417,-1.601997 -0.60699,-0.695999 -1.043,-1.526001 -1.30899,-2.489998 C 326.15302,63.799005 326.021,62.724 326.021,61.538003 l 0,-9.338001 -3.54898,0 0,-5.412003 3.50399,0 0,-5.810997 7.142,0 0,5.810997 5.19,0 0,5.412003 -5.19,0 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
id="svg_15"
|
||||
d="m 145.00999,36.869999 c -2.18299,0 -3.89199,1.573002 -3.89199,3.582001 0,2.116001 1.43899,3.536999 3.582,3.536999 0.183,0 0.35599,-0.017 0.51899,-0.05 -0.343,1.566002 -1.852,2.690002 -3.27799,2.915001 l -0.29001,0.046 0,3.376999 0.376,-0.036 c 1.73,-0.165001 3.439,-0.951 4.691,-2.157001 1.632,-1.572998 2.49501,-3.843998 2.49501,-6.568001 0,-2.691998 -1.76799,-4.646 -4.20301,-4.646 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
</g>
|
||||
<g
|
||||
id="svg_16">
|
||||
<path
|
||||
id="svg_17"
|
||||
d="m 46.488998,37.568001 -8.039997,0 0,-4.128002 c 0,-3.296997 -2.683002,-5.979 -5.98,-5.979 -3.297001,0 -5.979,2.683002 -5.979,5.979 l 0,4.128002 -8.040001,0 0,-4.128002 c 0,-7.73 6.288998,-14.019999 14.02,-14.019999 7.731002,0 14.02,6.289 14.02,14.019999 l 0,4.128002 -0.001,0 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f9a11d" />
|
||||
</g>
|
||||
<path
|
||||
id="svg_18"
|
||||
d="m 49.731998,37.568001 -34.524998,0 c -1.474001,0 -2.68,1.205997 -2.68,2.68 l 0,25.540001 c 0,1.473999 1.205999,2.68 2.68,2.68 l 34.524998,0 c 1.474003,0 2.68,-1.206001 2.68,-2.68 l 0,-25.540001 c 0,-1.474003 -1.205997,-2.68 -2.68,-2.68 z m -15.512997,16.769001 0,3.460995 c 0,0.966003 -0.784,1.749001 -1.749001,1.749001 -0.965001,0 -1.749001,-0.783997 -1.749001,-1.749001 l 0,-3.459995 c -1.076,-0.611 -1.803001,-1.764 -1.803001,-3.09 0,-1.962002 1.591,-3.552002 3.552002,-3.552002 1.961998,0 3.551998,1.591 3.551998,3.552002 0,1.325001 -0.727001,2.478001 -1.802998,3.089001 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#2c3c69" />
|
||||
<path
|
||||
id="svg_19"
|
||||
d="m 11.707001,33.759998 -8.331,0 c -1.351001,0 -2.446,-1.094997 -2.446,-2.445999 0,-1.351002 1.094999,-2.445999 2.446,-2.445999 l 8.331,0 c 1.351,0 2.445999,1.095001 2.445999,2.445999 0,1.350998 -1.096001,2.445999 -2.445999,2.445999 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f9a11d" />
|
||||
<path
|
||||
id="svg_20"
|
||||
d="m 17.575001,20.655001 c -0.546001,0 -1.097,-0.182001 -1.552,-0.557001 l -6.59,-5.418999 C 8.39,13.820999 8.239001,12.280001 9.098,11.236 9.956,10.193001 11.497,10.042 12.541001,10.9 l 6.59,5.419001 c 1.042999,0.858 1.194,2.399 0.334999,3.442999 -0.483,0.589001 -1.184,0.893002 -1.890999,0.893002 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f9a11d" />
|
||||
<path
|
||||
id="svg_21"
|
||||
d="m 32.469002,14.895 c -1.351002,0 -2.446003,-1.095001 -2.446003,-2.446001 l 0,-8.396999 c 0,-1.351 1.095001,-2.446 2.446003,-2.446 1.351002,0 2.445999,1.095 2.445999,2.446 l 0,8.396999 c 0,1.351 -1.095001,2.446001 -2.445999,2.446001 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f9a11d" />
|
||||
<g
|
||||
id="svg_22">
|
||||
<g
|
||||
id="svg_23">
|
||||
<path
|
||||
id="svg_24"
|
||||
d="M 47.362999,20.655001 C 46.655998,20.655001 45.956001,20.351 45.472,19.761999 44.613998,18.719 44.764,17.177 45.806999,16.319 l 6.59,-5.419001 c 1.044003,-0.858 2.585003,-0.706999 3.442997,0.336 0.858002,1.042999 0.708,2.584999 -0.334999,3.443001 l -6.589996,5.418999 C 48.459999,20.472999 47.91,20.655 47.362999,20.655 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f9a11d" />
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
id="svg_25"
|
||||
d="m 61.563004,33.759998 -8.410004,0 c -1.351002,0 -2.445999,-1.094997 -2.445999,-2.445999 0,-1.351002 1.094997,-2.445999 2.445999,-2.445999 l 8.410004,0 c 1.350998,0 2.445999,1.095001 2.445999,2.445999 0,1.350998 -1.095001,2.445999 -2.445999,2.445999 z"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#f9a11d" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 16 KiB |
5394
docs/img/overview.svg
Normal file
After Width: | Height: | Size: 218 KiB |
BIN
docs/img/traefik.icon.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
docs/img/traefik.logo.png
Normal file
After Width: | Height: | Size: 34 KiB |
1402
docs/index.md
971
docs/toml.md
Normal file
@@ -0,0 +1,971 @@
|
||||
|
||||
# Global configuration
|
||||
|
||||
## Main section
|
||||
|
||||
```toml
|
||||
# traefik.toml
|
||||
################################################################
|
||||
# Global configuration
|
||||
################################################################
|
||||
|
||||
# Traefik logs file
|
||||
# If not defined, logs to stdout
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# traefikLogsFile = "log/traefik.log"
|
||||
|
||||
# Access logs file
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# accessLogsFile = "log/access.log"
|
||||
|
||||
# Log level
|
||||
#
|
||||
# Optional
|
||||
# Default: "ERROR"
|
||||
#
|
||||
# logLevel = "ERROR"
|
||||
|
||||
# Backends throttle duration: minimum duration between 2 events from providers
|
||||
# before applying a new configuration. It avoids unnecessary reloads if multiples events
|
||||
# are sent in a short amount of time.
|
||||
#
|
||||
# Optional
|
||||
# Default: "2s"
|
||||
#
|
||||
# ProvidersThrottleDuration = "5s"
|
||||
|
||||
# If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used.
|
||||
# If you encounter 'too many open files' errors, you can either change this value, or change `ulimit` value.
|
||||
#
|
||||
# Optional
|
||||
# Default: http.DefaultMaxIdleConnsPerHost
|
||||
#
|
||||
# MaxIdleConnsPerHost = 200
|
||||
|
||||
# Entrypoints to be used by frontends that do not specify any entrypoint.
|
||||
# Each frontend can specify its own entrypoints.
|
||||
#
|
||||
# Optional
|
||||
# Default: ["http"]
|
||||
#
|
||||
# defaultEntryPoints = ["http", "https"]
|
||||
```
|
||||
|
||||
## Entrypoints definition
|
||||
|
||||
```toml
|
||||
# Entrypoints definition
|
||||
#
|
||||
# Optional
|
||||
# Default:
|
||||
# [entryPoints]
|
||||
# [entryPoints.http]
|
||||
# address = ":80"
|
||||
#
|
||||
# To redirect an http entrypoint to an https entrypoint (with SNI support):
|
||||
# [entryPoints]
|
||||
# [entryPoints.http]
|
||||
# address = ":80"
|
||||
# [entryPoints.http.redirect]
|
||||
# entryPoint = "https"
|
||||
# [entryPoints.https]
|
||||
# address = ":443"
|
||||
# [entryPoints.https.tls]
|
||||
# [[entryPoints.https.tls.certificates]]
|
||||
# CertFile = "integration/fixtures/https/snitest.com.cert"
|
||||
# KeyFile = "integration/fixtures/https/snitest.com.key"
|
||||
# [[entryPoints.https.tls.certificates]]
|
||||
# CertFile = "integration/fixtures/https/snitest.org.cert"
|
||||
# KeyFile = "integration/fixtures/https/snitest.org.key"
|
||||
#
|
||||
# To redirect an entrypoint rewriting the URL:
|
||||
# [entryPoints]
|
||||
# [entryPoints.http]
|
||||
# address = ":80"
|
||||
# [entryPoints.http.redirect]
|
||||
# regex = "^http://localhost/(.*)"
|
||||
# replacement = "http://mydomain/$1"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
```
|
||||
|
||||
## Retry configuration
|
||||
|
||||
```toml
|
||||
# Enable retry sending request if network error
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[retry]
|
||||
|
||||
# Number of attempts
|
||||
#
|
||||
# Optional
|
||||
# Default: (number servers in backend) -1
|
||||
#
|
||||
# attempts = 3
|
||||
|
||||
# Sets the maximum request body to be stored in memory in Mo
|
||||
#
|
||||
# Optional
|
||||
# Default: 2
|
||||
#
|
||||
# maxMem = 3
|
||||
```
|
||||
|
||||
## ACME (Let's Encrypt) configuration
|
||||
|
||||
```toml
|
||||
# Sample entrypoint configuration when using ACME
|
||||
[entryPoints]
|
||||
[entryPoints.https]
|
||||
address = ":443"
|
||||
[entryPoints.https.tls]
|
||||
|
||||
# Enable ACME (Let's Encrypt): automatic SSL
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[acme]
|
||||
|
||||
# Email address used for registration
|
||||
#
|
||||
# Required
|
||||
#
|
||||
email = "test@traefik.io"
|
||||
|
||||
# File used for certificates storage.
|
||||
# WARNING, if you use Traefik in Docker, don't forget to mount this file as a volume.
|
||||
#
|
||||
# Required
|
||||
#
|
||||
storageFile = "acme.json"
|
||||
|
||||
# Entrypoint to proxy acme challenge to.
|
||||
# WARNING, must point to an entrypoint on port 443
|
||||
#
|
||||
# Required
|
||||
#
|
||||
entryPoint = "https"
|
||||
|
||||
# Enable on demand certificate. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate.
|
||||
# WARNING, TLS handshakes will be slow when requesting a hostname certificate for the first time, this can leads to DoS attacks.
|
||||
# WARNING, Take note that Let's Encrypt have rate limiting: https://community.letsencrypt.org/t/quick-start-guide/1631
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# onDemand = true
|
||||
|
||||
# CA server to use
|
||||
# Uncomment the line to run on the staging let's encrypt server
|
||||
# Leave comment to go to prod
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# caServer = "https://acme-staging.api.letsencrypt.org/directory"
|
||||
|
||||
# Domains list
|
||||
# You can provide SANs (alternative domains) to each main domain
|
||||
# All domains must have A/AAAA records pointing to Traefik
|
||||
# WARNING, Take note that Let's Encrypt have rate limiting: https://community.letsencrypt.org/t/quick-start-guide/1631
|
||||
# Each domain & SANs will lead to a certificate request.
|
||||
#
|
||||
# [[acme.domains]]
|
||||
# main = "local1.com"
|
||||
# sans = ["test1.local1.com", "test2.local1.com"]
|
||||
# [[acme.domains]]
|
||||
# main = "local2.com"
|
||||
# sans = ["test1.local2.com", "test2x.local2.com"]
|
||||
# [[acme.domains]]
|
||||
# main = "local3.com"
|
||||
# [[acme.domains]]
|
||||
# main = "local4.com"
|
||||
[[acme.domains]]
|
||||
main = "local1.com"
|
||||
sans = ["test1.local1.com", "test2.local1.com"]
|
||||
[[acme.domains]]
|
||||
main = "local3.com"
|
||||
[[acme.domains]]
|
||||
main = "local4.com"
|
||||
```
|
||||
|
||||
# Configuration backends
|
||||
|
||||
## File backend
|
||||
|
||||
Like any other reverse proxy, Træfɪk can be configured with a file. You have two choices:
|
||||
|
||||
- simply add your configuration at the end of the global configuration file `traefik.toml` :
|
||||
|
||||
```toml
|
||||
# traefik.toml
|
||||
logLevel = "DEBUG"
|
||||
defaultEntryPoints = ["http", "https"]
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
[entryPoints.http.redirect]
|
||||
entryPoint = "https"
|
||||
[entryPoints.https]
|
||||
address = ":443"
|
||||
[entryPoints.https.tls]
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
CertFile = "integration/fixtures/https/snitest.com.cert"
|
||||
KeyFile = "integration/fixtures/https/snitest.com.key"
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
CertFile = "integration/fixtures/https/snitest.org.cert"
|
||||
KeyFile = "integration/fixtures/https/snitest.org.key"
|
||||
|
||||
[file]
|
||||
|
||||
# rules
|
||||
[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.circuitbreaker]
|
||||
expression = "NetworkErrorRatio() > 0.5"
|
||||
[backends.backend1.servers.server1]
|
||||
url = "http://172.17.0.2:80"
|
||||
weight = 10
|
||||
[backends.backend1.servers.server2]
|
||||
url = "http://172.17.0.3:80"
|
||||
weight = 1
|
||||
[backends.backend2]
|
||||
[backends.backend1.maxconn]
|
||||
amount = 10
|
||||
extractorfunc = "request.host"
|
||||
[backends.backend2.LoadBalancer]
|
||||
method = "drr"
|
||||
[backends.backend2.servers.server1]
|
||||
url = "http://172.17.0.4:80"
|
||||
weight = 1
|
||||
[backends.backend2.servers.server2]
|
||||
url = "http://172.17.0.5:80"
|
||||
weight = 2
|
||||
|
||||
[frontends]
|
||||
[frontends.frontend1]
|
||||
backend = "backend2"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host:test.localhost"
|
||||
[frontends.frontend2]
|
||||
backend = "backend1"
|
||||
passHostHeader = true
|
||||
entrypoints = ["https"] # overrides defaultEntryPoints
|
||||
[frontends.frontend2.routes.test_1]
|
||||
rule = "Host:{subdomain:[a-z]+}.localhost"
|
||||
[frontends.frontend3]
|
||||
entrypoints = ["http", "https"] # overrides defaultEntryPoints
|
||||
backend = "backend2"
|
||||
rule = "Path:/test"
|
||||
```
|
||||
|
||||
- or put your rules in a separate file, for example `rules.toml`:
|
||||
|
||||
```toml
|
||||
# traefik.toml
|
||||
logLevel = "DEBUG"
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
[entryPoints.http.redirect]
|
||||
entryPoint = "https"
|
||||
[entryPoints.https]
|
||||
address = ":443"
|
||||
[entryPoints.https.tls]
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
CertFile = "integration/fixtures/https/snitest.com.cert"
|
||||
KeyFile = "integration/fixtures/https/snitest.com.key"
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
CertFile = "integration/fixtures/https/snitest.org.cert"
|
||||
KeyFile = "integration/fixtures/https/snitest.org.key"
|
||||
|
||||
[file]
|
||||
filename = "rules.toml"
|
||||
```
|
||||
|
||||
```toml
|
||||
# rules.toml
|
||||
[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.circuitbreaker]
|
||||
expression = "NetworkErrorRatio() > 0.5"
|
||||
[backends.backend1.servers.server1]
|
||||
url = "http://172.17.0.2:80"
|
||||
weight = 10
|
||||
[backends.backend1.servers.server2]
|
||||
url = "http://172.17.0.3:80"
|
||||
weight = 1
|
||||
[backends.backend2]
|
||||
[backends.backend1.maxconn]
|
||||
amount = 10
|
||||
extractorfunc = "request.host"
|
||||
[backends.backend2.LoadBalancer]
|
||||
method = "drr"
|
||||
[backends.backend2.servers.server1]
|
||||
url = "http://172.17.0.4:80"
|
||||
weight = 1
|
||||
[backends.backend2.servers.server2]
|
||||
url = "http://172.17.0.5:80"
|
||||
weight = 2
|
||||
|
||||
[frontends]
|
||||
[frontends.frontend1]
|
||||
backend = "backend2"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host:test.localhost"
|
||||
[frontends.frontend2]
|
||||
backend = "backend1"
|
||||
passHostHeader = true
|
||||
entrypoints = ["https"] # overrides defaultEntryPoints
|
||||
[frontends.frontend2.routes.test_1]
|
||||
rule = "Host:{subdomain:[a-z]+}.localhost"
|
||||
[frontends.frontend3]
|
||||
entrypoints = ["http", "https"] # overrides defaultEntryPoints
|
||||
backend = "backend2"
|
||||
rule = "Path:/test"
|
||||
```
|
||||
|
||||
If you want Træfɪk to watch file changes automatically, just add:
|
||||
|
||||
```toml
|
||||
[file]
|
||||
watch = true
|
||||
```
|
||||
|
||||
## API backend
|
||||
|
||||
Træfik can be configured using a restful api.
|
||||
To enable it:
|
||||
|
||||
```toml
|
||||
[web]
|
||||
address = ":8080"
|
||||
|
||||
# SSL certificate and key used
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# CertFile = "traefik.crt"
|
||||
# KeyFile = "traefik.key"
|
||||
#
|
||||
# Set REST API to read-only mode
|
||||
#
|
||||
# Optional
|
||||
# ReadOnly = false
|
||||
```
|
||||
|
||||
- `/`: provides a simple HTML frontend of Træfik
|
||||
|
||||

|
||||

|
||||
|
||||
- `/health`: `GET` json metrics
|
||||
|
||||
```sh
|
||||
$ curl -s "http://localhost:8080/health" | jq .
|
||||
{
|
||||
// Træfɪk PID
|
||||
"pid": 2458,
|
||||
// Træfɪk server uptime (formated time)
|
||||
"uptime": "39m6.885931127s",
|
||||
// Træfɪk server uptime in seconds
|
||||
"uptime_sec": 2346.885931127,
|
||||
// current server date
|
||||
"time": "2015-10-07 18:32:24.362238909 +0200 CEST",
|
||||
// current server date in seconds
|
||||
"unixtime": 1444235544,
|
||||
// count HTTP response status code in realtime
|
||||
"status_code_count": {
|
||||
"502": 1
|
||||
},
|
||||
// count HTTP response status code since Træfɪk started
|
||||
"total_status_code_count": {
|
||||
"200": 7,
|
||||
"404": 21,
|
||||
"502": 13
|
||||
},
|
||||
// count HTTP response
|
||||
"count": 1,
|
||||
// count HTTP response
|
||||
"total_count": 41,
|
||||
// sum of all response time (formated time)
|
||||
"total_response_time": "35.456865605s",
|
||||
// sum of all response time in seconds
|
||||
"total_response_time_sec": 35.456865605,
|
||||
// average response time (formated time)
|
||||
"average_response_time": "864.8016ms",
|
||||
// average response time in seconds
|
||||
"average_response_time_sec": 0.8648016000000001
|
||||
}
|
||||
```
|
||||
|
||||
- `/api`: `GET` configuration for all providers
|
||||
|
||||
```sh
|
||||
$ curl -s "http://localhost:8080/api" | jq .
|
||||
{
|
||||
"file": {
|
||||
"frontends": {
|
||||
"frontend2": {
|
||||
"routes": {
|
||||
"test_2": {
|
||||
"rule": "Path:/test"
|
||||
}
|
||||
},
|
||||
"backend": "backend1"
|
||||
},
|
||||
"frontend1": {
|
||||
"routes": {
|
||||
"test_1": {
|
||||
"rule": "Host:test.localhost"
|
||||
}
|
||||
},
|
||||
"backend": "backend2"
|
||||
}
|
||||
},
|
||||
"backends": {
|
||||
"backend2": {
|
||||
"loadBalancer": {
|
||||
"method": "drr"
|
||||
},
|
||||
"servers": {
|
||||
"server2": {
|
||||
"weight": 2,
|
||||
"URL": "http://172.17.0.5:80"
|
||||
},
|
||||
"server1": {
|
||||
"weight": 1,
|
||||
"url": "http://172.17.0.4:80"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backend1": {
|
||||
"loadBalancer": {
|
||||
"method": "wrr"
|
||||
},
|
||||
"circuitBreaker": {
|
||||
"expression": "NetworkErrorRatio() > 0.5"
|
||||
},
|
||||
"servers": {
|
||||
"server2": {
|
||||
"weight": 1,
|
||||
"url": "http://172.17.0.3:80"
|
||||
},
|
||||
"server1": {
|
||||
"weight": 10,
|
||||
"url": "http://172.17.0.2:80"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `/api/providers`: `GET` providers
|
||||
- `/api/providers/{provider}`: `GET` or `PUT` provider
|
||||
- `/api/providers/{provider}/backends`: `GET` backends
|
||||
- `/api/providers/{provider}/backends/{backend}`: `GET` a backend
|
||||
- `/api/providers/{provider}/backends/{backend}/servers`: `GET` servers in a backend
|
||||
- `/api/providers/{provider}/backends/{backend}/servers/{server}`: `GET` a server in a backend
|
||||
- `/api/providers/{provider}/frontends`: `GET` frontends
|
||||
- `/api/providers/{provider}/frontends/{frontend}`: `GET` a frontend
|
||||
- `/api/providers/{provider}/frontends/{frontend}/routes`: `GET` routes in a frontend
|
||||
- `/api/providers/{provider}/frontends/{frontend}/routes/{route}`: `GET` a route in a frontend
|
||||
|
||||
|
||||
## Docker backend
|
||||
|
||||
Træfɪk can be configured to use Docker as a backend configuration:
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# Docker configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable Docker configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[docker]
|
||||
|
||||
# Docker server endpoint. Can be a tcp or a unix socket endpoint.
|
||||
#
|
||||
# Required
|
||||
#
|
||||
endpoint = "unix:///var/run/docker.sock"
|
||||
|
||||
# Default domain used.
|
||||
# Can be overridden by setting the "traefik.domain" label on a container.
|
||||
#
|
||||
# Required
|
||||
#
|
||||
domain = "docker.localhost"
|
||||
|
||||
# Enable watch docker changes
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
watch = true
|
||||
|
||||
# Override default configuration template. For advanced users :)
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# filename = "docker.tmpl"
|
||||
|
||||
# Enable docker TLS connection
|
||||
#
|
||||
# [docker.tls]
|
||||
# ca = "/etc/ssl/ca.crt"
|
||||
# cert = "/etc/ssl/docker.crt"
|
||||
# key = "/etc/ssl/docker.key"
|
||||
# insecureskipverify = true
|
||||
```
|
||||
|
||||
Labels can be used on containers to override default behaviour:
|
||||
|
||||
- `traefik.backend=foo`: assign the container to `foo` backend
|
||||
- `traefik.port=80`: register this port. Useful when the container exposes multiples ports.
|
||||
- `traefik.protocol=https`: override the default `http` protocol
|
||||
- `traefik.weight=10`: assign this weight to the container
|
||||
- `traefik.enable=false`: disable this container in Træfɪk
|
||||
- `traefik.frontend.rule=Host:test.traefik.io`: override the default frontend rule (Default: `Host:{containerName}.{domain}`).
|
||||
- `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend.
|
||||
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
|
||||
* `traefik.domain=traefik.localhost`: override the default domain
|
||||
|
||||
|
||||
## Marathon backend
|
||||
|
||||
Træfɪk can be configured to use Marathon as a backend configuration:
|
||||
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# Mesos/Marathon configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable Marathon configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[marathon]
|
||||
|
||||
# Marathon server endpoint.
|
||||
# You can also specify multiple endpoint for Marathon:
|
||||
# endpoint := "http://10.241.1.71:8080,10.241.1.72:8080,10.241.1.73:8080"
|
||||
#
|
||||
# Required
|
||||
#
|
||||
endpoint = "http://127.0.0.1:8080"
|
||||
|
||||
# Enable watch Marathon changes
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
watch = true
|
||||
|
||||
# Default domain used.
|
||||
# Can be overridden by setting the "traefik.domain" label on an application.
|
||||
#
|
||||
# Required
|
||||
#
|
||||
domain = "marathon.localhost"
|
||||
|
||||
# Override default configuration template. For advanced users :)
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# filename = "marathon.tmpl"
|
||||
|
||||
# Expose Marathon apps by default in traefik
|
||||
#
|
||||
# Optional
|
||||
# Default: false
|
||||
#
|
||||
# ExposedByDefault = true
|
||||
|
||||
# Enable Marathon basic authentication
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# [marathon.basic]
|
||||
# httpBasicAuthUser = "foo"
|
||||
# httpBasicPassword = "bar"
|
||||
|
||||
# TLS client configuration. https://golang.org/pkg/crypto/tls/#Config
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# [marathon.TLS]
|
||||
# InsecureSkipVerify = true
|
||||
```
|
||||
|
||||
Labels can be used on containers to override default behaviour:
|
||||
|
||||
- `traefik.backend=foo`: assign the application to `foo` backend
|
||||
- `traefik.portIndex=1`: register port by index in the application's ports array. Useful when the application exposes multiple ports.
|
||||
- `traefik.port=80`: register the explicit application port value. Cannot be used alongside `traefik.portIndex`.
|
||||
- `traefik.protocol=https`: override the default `http` protocol
|
||||
- `traefik.weight=10`: assign this weight to the application
|
||||
- `traefik.enable=false`: disable this application in Træfɪk
|
||||
- `traefik.frontend.rule=Host:test.traefik.io`: override the default frontend rule (Default: `Host:{containerName}.{domain}`).
|
||||
- `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend.
|
||||
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
|
||||
* `traefik.domain=traefik.localhost`: override the default domain
|
||||
|
||||
|
||||
## Kubernetes Ingress backend
|
||||
|
||||
|
||||
Træfɪk can be configured to use Kubernetes Ingress as a backend configuration:
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# Kubernetes Ingress configuration backend
|
||||
################################################################
|
||||
# Enable Kubernetes Ingress configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[kubernetes]
|
||||
|
||||
# Kubernetes server endpoint
|
||||
#
|
||||
# When deployed as a replication controller in Kubernetes,
|
||||
# Traefik will use env variable KUBERNETES_SERVICE_HOST
|
||||
# and KUBERNETES_SERVICE_PORT_HTTPS as endpoint
|
||||
# Secure token will be found in /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
# and SSL CA cert in /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# endpoint = "http://localhost:8080"
|
||||
```
|
||||
|
||||
You can find here an example [ingress](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s.ingress.yaml) and [replication controller](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s.rc.yaml).
|
||||
|
||||
## Consul backend
|
||||
|
||||
Træfɪk can be configured to use Consul as a backend configuration:
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# Consul KV configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable Consul KV configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[consul]
|
||||
|
||||
# Consul server endpoint
|
||||
#
|
||||
# Required
|
||||
#
|
||||
endpoint = "127.0.0.1:8500"
|
||||
|
||||
# Enable watch Consul changes
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
watch = true
|
||||
|
||||
# Prefix used for KV store.
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
prefix = "traefik"
|
||||
|
||||
# Override default configuration template. For advanced users :)
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# filename = "consul.tmpl"
|
||||
|
||||
# Enable consul TLS connection
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# [consul.tls]
|
||||
# ca = "/etc/ssl/ca.crt"
|
||||
# cert = "/etc/ssl/consul.crt"
|
||||
# key = "/etc/ssl/consul.key"
|
||||
# insecureskipverify = true
|
||||
```
|
||||
|
||||
Please refer to the [Key Value storage structure](#key-value-storage-structure) section to get documentation en traefik KV structure.
|
||||
|
||||
## Consul catalog backend
|
||||
|
||||
Træfɪk can be configured to use service discovery catalog of Consul as a backend configuration:
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# Consul Catalog configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable Consul Catalog configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[consulCatalog]
|
||||
|
||||
# Consul server endpoint
|
||||
#
|
||||
# Required
|
||||
#
|
||||
endpoint = "127.0.0.1:8500"
|
||||
|
||||
# Default domain used.
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
domain = "consul.localhost"
|
||||
|
||||
# Prefix for Consul catalog tags
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
prefix = "traefik"
|
||||
```
|
||||
|
||||
This backend will create routes matching on hostname based on the service name
|
||||
used in consul.
|
||||
|
||||
Additional settings can be defined using Consul Catalog tags:
|
||||
|
||||
- ```traefik.enable=false```: disable this container in Træfɪk
|
||||
- ```traefik.protocol=https```: override the default `http` protocol
|
||||
- ```traefik.backend.weight=10```: assign this weight to the container
|
||||
- ```traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5```
|
||||
- ```traefik.backend.loadbalancer=drr```: override the default load balancing mode
|
||||
- ```traefik.frontend.rule=Host:test.traefik.io```: override the default frontend rule (Default: `Host:{containerName}.{domain}`).
|
||||
- ```traefik.frontend.passHostHeader=true```: forward client `Host` header to the backend.
|
||||
- ```traefik.frontend.entryPoints=http,https```: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
|
||||
|
||||
## Etcd backend
|
||||
|
||||
Træfɪk can be configured to use Etcd as a backend configuration:
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# Etcd configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable Etcd configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[etcd]
|
||||
|
||||
# Etcd server endpoint
|
||||
#
|
||||
# Required
|
||||
#
|
||||
endpoint = "127.0.0.1:4001"
|
||||
|
||||
# Enable watch Etcd changes
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
watch = true
|
||||
|
||||
# Prefix used for KV store.
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
prefix = "/traefik"
|
||||
|
||||
# Override default configuration template. For advanced users :)
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# filename = "etcd.tmpl"
|
||||
|
||||
# Enable etcd TLS connection
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# [etcd.tls]
|
||||
# ca = "/etc/ssl/ca.crt"
|
||||
# cert = "/etc/ssl/etcd.crt"
|
||||
# key = "/etc/ssl/etcd.key"
|
||||
# insecureskipverify = true
|
||||
```
|
||||
|
||||
Please refer to the [Key Value storage structure](#key-value-storage-structure) section to get documentation en traefik KV structure.
|
||||
|
||||
|
||||
## Zookeeper backend
|
||||
|
||||
Træfɪk can be configured to use Zookeeper as a backend configuration:
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# Zookeeper configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable Zookeeperconfiguration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[zookeeper]
|
||||
|
||||
# Zookeeper server endpoint
|
||||
#
|
||||
# Required
|
||||
#
|
||||
endpoint = "127.0.0.1:2181"
|
||||
|
||||
# Enable watch Zookeeper changes
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
watch = true
|
||||
|
||||
# Prefix used for KV store.
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
prefix = "/traefik"
|
||||
|
||||
# Override default configuration template. For advanced users :)
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# filename = "zookeeper.tmpl"
|
||||
```
|
||||
|
||||
Please refer to the [Key Value storage structure](#key-value-storage-structure) section to get documentation en traefik KV structure.
|
||||
|
||||
## BoltDB backend
|
||||
|
||||
Træfɪk can be configured to use BoltDB as a backend configuration:
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# BoltDB configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable BoltDB configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[boltdb]
|
||||
|
||||
# BoltDB file
|
||||
#
|
||||
# Required
|
||||
#
|
||||
endpoint = "/my.db"
|
||||
|
||||
# Enable watch BoltDB changes
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
watch = true
|
||||
|
||||
# Prefix used for KV store.
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
prefix = "/traefik"
|
||||
|
||||
# Override default configuration template. For advanced users :)
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# filename = "boltdb.tmpl"
|
||||
```
|
||||
|
||||
Please refer to the [Key Value storage structure](#key-value-storage-structure) section to get documentation en traefik KV structure.
|
||||
|
||||
## Key-value storage structure
|
||||
|
||||
The Keys-Values structure should look (using `prefix = "/traefik"`):
|
||||
|
||||
- backend 1
|
||||
|
||||
| Key | Value |
|
||||
|--------------------------------------------------------|-----------------------------|
|
||||
| `/traefik/backends/backend1/circuitbreaker/expression` | `NetworkErrorRatio() > 0.5` |
|
||||
| `/traefik/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
|
||||
| `/traefik/backends/backend1/servers/server1/weight` | `10` |
|
||||
| `/traefik/backends/backend1/servers/server2/url` | `http://172.17.0.3:80` |
|
||||
| `/traefik/backends/backend1/servers/server2/weight` | `1` |
|
||||
|
||||
- backend 2
|
||||
|
||||
| Key | Value |
|
||||
|-----------------------------------------------------|------------------------|
|
||||
| `/traefik/backends/backend2/maxconn/amount` | `10` |
|
||||
| `/traefik/backends/backend2/maxconn/extractorfunc` | `request.host` |
|
||||
| `/traefik/backends/backend2/loadbalancer/method` | `drr` |
|
||||
| `/traefik/backends/backend2/servers/server1/url` | `http://172.17.0.4:80` |
|
||||
| `/traefik/backends/backend2/servers/server1/weight` | `1` |
|
||||
| `/traefik/backends/backend2/servers/server2/url` | `http://172.17.0.5:80` |
|
||||
| `/traefik/backends/backend2/servers/server2/weight` | `2` |
|
||||
|
||||
- frontend 1
|
||||
|
||||
| Key | Value |
|
||||
|---------------------------------------------------|-----------------------|
|
||||
| `/traefik/frontends/frontend1/backend` | `backend2` |
|
||||
| `/traefik/frontends/frontend1/routes/test_1/rule` | `Host:test.localhost` |
|
||||
|
||||
- frontend 2
|
||||
|
||||
| Key | Value |
|
||||
|----------------------------------------------------|--------------|
|
||||
| `/traefik/frontends/frontend2/backend` | `backend1` |
|
||||
| `/traefik/frontends/frontend2/passHostHeader` | `true` |
|
||||
| `/traefik/frontends/frontend2/entrypoints` | `http,https` |
|
||||
| `/traefik/frontends/frontend2/routes/test_2/rule` | `Path:/test` |
|
||||
|
||||
## Atomic configuration changes
|
||||
|
||||
The [Etcd](https://github.com/coreos/etcd/issues/860) and [Consul](https://github.com/hashicorp/consul/issues/886) backends do not support updating multiple keys atomically. As a result, it may be possible for Træfɪk to read an intermediate configuration state despite judicious use of the `--providersThrottleDuration` flag. To solve this problem, Træfɪk supports a special key called `/traefik/alias`. If set, Træfɪk use the value as an alternative key prefix.
|
||||
|
||||
Given the key structure below, Træfɪk will use the `http://172.17.0.2:80` as its only backend (frontend keys have been omitted for brevity).
|
||||
|
||||
| Key | Value |
|
||||
|-------------------------------------------------------------------------|-----------------------------|
|
||||
| `/traefik/alias` | `/traefik_configurations/1` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/weight` | `10` |
|
||||
|
||||
When an atomic configuration change is required, you may write a new configuration at an alternative prefix. Here, although the `/traefik_configurations/2/...` keys have been set, the old configuration is still active because the `/traefik/alias` key still points to `/traefik_configurations/1`:
|
||||
|
||||
| Key | Value |
|
||||
|-------------------------------------------------------------------------|-----------------------------|
|
||||
| `/traefik/alias` | `/traefik_configurations/1` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/weight` | `10` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server1/weight` | `5` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server2/url` | `http://172.17.0.3:80` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server2/weight` | `5` |
|
||||
|
||||
Once the `/traefik/alias` key is updated, the new `/traefik_configurations/2` configuration becomes active atomically. Here, we have a 50% balance between the `http://172.17.0.3:80` and the `http://172.17.0.4:80` hosts while no traffic is sent to the `172.17.0.2:80` host:
|
||||
|
||||
| Key | Value |
|
||||
|-------------------------------------------------------------------------|-----------------------------|
|
||||
| `/traefik/alias` | `/traefik_configurations/2` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/weight` | `10` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server1/url` | `http://172.17.0.3:80` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server1/weight` | `5` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server2/url` | `http://172.17.0.4:80` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server2/weight` | `5` |
|
||||
|
||||
Note that Træfɪk *will not watch for key changes in the `/traefik_configurations` prefix*. It will only watch for changes in the `/traefik` prefix. Further, if the `/traefik/alias` key is set, all other sibling keys with the `/traefik` prefix are ignored.
|
||||
|
98
docs/user-guide/examples.md
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
# Examples
|
||||
|
||||
You will find here some configuration examples of Træfɪk.
|
||||
|
||||
## HTTP only
|
||||
|
||||
```
|
||||
defaultEntryPoints = ["http"]
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
```
|
||||
|
||||
## HTTP + HTTPS (with SNI)
|
||||
|
||||
```
|
||||
defaultEntryPoints = ["http", "https"]
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
[entryPoints.https]
|
||||
address = ":443"
|
||||
[entryPoints.https.tls]
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
CertFile = "integration/fixtures/https/snitest.com.cert"
|
||||
KeyFile = "integration/fixtures/https/snitest.com.key"
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
CertFile = "integration/fixtures/https/snitest.org.cert"
|
||||
KeyFile = "integration/fixtures/https/snitest.org.key"
|
||||
```
|
||||
|
||||
## HTTP redirect on HTTPS
|
||||
|
||||
```
|
||||
defaultEntryPoints = ["http", "https"]
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
[entryPoints.http.redirect]
|
||||
entryPoint = "https"
|
||||
[entryPoints.https]
|
||||
address = ":443"
|
||||
[entryPoints.https.tls]
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
certFile = "tests/traefik.crt"
|
||||
keyFile = "tests/traefik.key"
|
||||
```
|
||||
|
||||
## Let's Encrypt support
|
||||
|
||||
```
|
||||
[entryPoints]
|
||||
[entryPoints.https]
|
||||
address = ":443"
|
||||
[entryPoints.https.tls]
|
||||
# certs used as default certs
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
certFile = "tests/traefik.crt"
|
||||
keyFile = "tests/traefik.key"
|
||||
[acme]
|
||||
email = "test@traefik.io"
|
||||
storageFile = "acme.json"
|
||||
onDemand = true
|
||||
caServer = "http://172.18.0.1:4000/directory"
|
||||
entryPoint = "https"
|
||||
|
||||
[[acme.domains]]
|
||||
main = "local1.com"
|
||||
sans = ["test1.local1.com", "test2.local1.com"]
|
||||
[[acme.domains]]
|
||||
main = "local2.com"
|
||||
sans = ["test1.local2.com", "test2x.local2.com"]
|
||||
[[acme.domains]]
|
||||
main = "local3.com"
|
||||
[[acme.domains]]
|
||||
main = "local4.com"
|
||||
```
|
||||
|
||||
## Override entrypoints in frontends
|
||||
|
||||
```
|
||||
[frontends]
|
||||
[frontends.frontend1]
|
||||
backend = "backend2"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host:test.localhost"
|
||||
[frontends.frontend2]
|
||||
backend = "backend1"
|
||||
passHostHeader = true
|
||||
entrypoints = ["https"] # overrides defaultEntryPoints
|
||||
[frontends.frontend2.routes.test_1]
|
||||
rule = "Host:{subdomain:[a-z]+}.localhost"
|
||||
[frontends.frontend3]
|
||||
entrypoints = ["http", "https"] # overrides defaultEntryPoints
|
||||
backend = "backend2"
|
||||
rule = "Path:/test"
|
||||
```
|
170
docs/user-guide/swarm.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Swarm cluster
|
||||
|
||||
This section explains how to create a multi-host [swarm](https://docs.docker.com/swarm) cluster using [docker-machine](https://docs.docker.com/machine/) and how to deploy Træfɪk on it.
|
||||
The cluster will be made of:
|
||||
|
||||
- 2 servers
|
||||
- 1 swarm master
|
||||
- 2 swarm nodes
|
||||
- 1 [overlay](https://docs.docker.com/engine/userguide/networking/dockernetworks/#an-overlay-network) network (multi-host networking)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. You will need to install [docker-machine](https://docs.docker.com/machine/)
|
||||
2. You will need the latest [VirtualBox](https://www.virtualbox.org/wiki/Downloads)
|
||||
|
||||
## Cluster provisioning
|
||||
|
||||
We will first follow [this guide](https://docs.docker.com/engine/userguide/networking/get-started-overlay/) to create the cluster.
|
||||
|
||||
### Create machine `mh-keystore`
|
||||
|
||||
This machine will be the service registry of our cluster.
|
||||
|
||||
```sh
|
||||
docker-machine create -d virtualbox mh-keystore
|
||||
```
|
||||
|
||||
Then we install the service registry [Consul](https://consul.io) on this machine:
|
||||
|
||||
```sh
|
||||
eval "$(docker-machine env mh-keystore)"
|
||||
docker run -d \
|
||||
-p "8500:8500" \
|
||||
-h "consul" \
|
||||
progrium/consul -server -bootstrap
|
||||
```
|
||||
|
||||
### Create machine `mhs-demo0`
|
||||
|
||||
This machine will have a swarm master and a swarm agent on it.
|
||||
|
||||
```sh
|
||||
docker-machine create -d virtualbox \
|
||||
--swarm --swarm-master \
|
||||
--swarm-discovery="consul://$(docker-machine ip mh-keystore):8500" \
|
||||
--engine-opt="cluster-store=consul://$(docker-machine ip mh-keystore):8500" \
|
||||
--engine-opt="cluster-advertise=eth1:2376" \
|
||||
mhs-demo0
|
||||
```
|
||||
|
||||
### Create machine `mhs-demo1`
|
||||
|
||||
This machine will have a swarm agent on it.
|
||||
|
||||
```sh
|
||||
docker-machine create -d virtualbox \
|
||||
--swarm \
|
||||
--swarm-discovery="consul://$(docker-machine ip mh-keystore):8500" \
|
||||
--engine-opt="cluster-store=consul://$(docker-machine ip mh-keystore):8500" \
|
||||
--engine-opt="cluster-advertise=eth1:2376" \
|
||||
mhs-demo1
|
||||
```
|
||||
|
||||
### Create the overlay Network
|
||||
|
||||
Create the overlay network on the swarm master:
|
||||
|
||||
```sh
|
||||
eval $(docker-machine env --swarm mhs-demo0)
|
||||
docker network create --driver overlay --subnet=10.0.9.0/24 my-net
|
||||
```
|
||||
|
||||
## Deploy Træfɪk
|
||||
|
||||
Deploy Træfɪk:
|
||||
|
||||
```sh
|
||||
docker $(docker-machine config mhs-demo0) run \
|
||||
-d \
|
||||
-p 80:80 -p 8080:8080 \
|
||||
--net=my-net \
|
||||
-v /var/lib/boot2docker/:/ssl \
|
||||
traefik \
|
||||
-l DEBUG \
|
||||
-c /dev/null \
|
||||
--docker \
|
||||
--docker.domain traefik \
|
||||
--docker.endpoint tcp://$(docker-machine ip mhs-demo0):3376 \
|
||||
--docker.tls \
|
||||
--docker.tls.ca /ssl/ca.pem \
|
||||
--docker.tls.cert /ssl/server.pem \
|
||||
--docker.tls.key /ssl/server-key.pem \
|
||||
--docker.tls.insecureSkipVerify \
|
||||
--docker.watch \
|
||||
--web
|
||||
```
|
||||
|
||||
Let's explain this command:
|
||||
|
||||
- `-p 80:80 -p 8080:8080`: we bind ports 80 and 8080
|
||||
- `--net=my-net`: run the container on the network my-net
|
||||
- `-v /var/lib/boot2docker/:/ssl`: mount the ssl keys generated by docker-machine
|
||||
- `-c /dev/null`: empty config file
|
||||
- `--docker`: enable docker backend
|
||||
- `--docker.endpoint tcp://172.18.0.1:3376`: connect to the swarm master using the docker_gwbridge network
|
||||
- `--docker.tls`: enable TLS using the docker-machine keys
|
||||
- `--web`: activate the webUI on port 8080
|
||||
|
||||
## Deploy your apps
|
||||
|
||||
We can now deploy our app on the cluster, here [whoami](https://github.com/emilevauge/whoami), a simple web server in GO, on the network `my-net`:
|
||||
|
||||
```sh
|
||||
eval $(docker-machine env --swarm mhs-demo0)
|
||||
docker run -d --name=whoami0 --net=my-net --env="constraint:node==mhs-demo0" emilevauge/whoami
|
||||
docker run -d --name=whoami1 --net=my-net --env="constraint:node==mhs-demo1" emilevauge/whoami
|
||||
```
|
||||
|
||||
Check that everything is started:
|
||||
|
||||
```sh
|
||||
docker ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
ba2c21488299 emilevauge/whoami "/whoamI" 8 seconds ago Up 9 seconds 80/tcp mhs-demo1/whoami1
|
||||
8147a7746e7a emilevauge/whoami "/whoamI" 19 seconds ago Up 20 seconds 80/tcp mhs-demo0/whoami0
|
||||
8fbc39271b4c traefik "/traefik -l DEBUG -c" 36 seconds ago Up 37 seconds 192.168.99.101:80->80/tcp, 192.168.99.101:8080->8080/tcp mhs-demo0/serene_bhabha
|
||||
```
|
||||
|
||||
## Access to your apps through Træfɪk
|
||||
|
||||
```sh
|
||||
curl -H Host:whoami0.traefik http://$(docker-machine ip mhs-demo0)
|
||||
Hostname: 8147a7746e7a
|
||||
IP: 127.0.0.1
|
||||
IP: ::1
|
||||
IP: 10.0.9.3
|
||||
IP: fe80::42:aff:fe00:903
|
||||
IP: 172.18.0.3
|
||||
IP: fe80::42:acff:fe12:3
|
||||
GET / HTTP/1.1
|
||||
Host: 10.0.9.3:80
|
||||
User-Agent: curl/7.35.0
|
||||
Accept: */*
|
||||
Accept-Encoding: gzip
|
||||
X-Forwarded-For: 192.168.99.1
|
||||
X-Forwarded-Host: 10.0.9.3:80
|
||||
X-Forwarded-Proto: http
|
||||
X-Forwarded-Server: 8fbc39271b4c
|
||||
|
||||
curl -H Host:whoami1.traefik http://$(docker-machine ip mhs-demo0)
|
||||
Hostname: ba2c21488299
|
||||
IP: 127.0.0.1
|
||||
IP: ::1
|
||||
IP: 10.0.9.4
|
||||
IP: fe80::42:aff:fe00:904
|
||||
IP: 172.18.0.2
|
||||
IP: fe80::42:acff:fe12:2
|
||||
GET / HTTP/1.1
|
||||
Host: 10.0.9.4:80
|
||||
User-Agent: curl/7.35.0
|
||||
Accept: */*
|
||||
Accept-Encoding: gzip
|
||||
X-Forwarded-For: 192.168.99.1
|
||||
X-Forwarded-Host: 10.0.9.4:80
|
||||
X-Forwarded-Proto: http
|
||||
X-Forwarded-Server: 8fbc39271b4c
|
||||
```
|
||||
|
||||

|
||||
|
2
examples/accessLog/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
exampleHandler
|
||||
exampleHandler.exe
|
46
examples/accessLog/exampleHandler.go
Normal 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
@@ -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
|
||||
|
40
examples/accessLog/runExample.sh
Executable 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
|
37
examples/accessLog/traefik.ab.toml
Normal 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"
|
42
examples/accessLog/traefik.example.toml
Normal 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"
|
4
examples/compose-etcd.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
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']
|
17
examples/compose-k8s.yaml
Normal 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']
|
@@ -6,7 +6,7 @@ zk:
|
||||
ZK_ID: 1
|
||||
|
||||
master:
|
||||
image: mesosphere/mesos-master:0.26.0-0.2.145.ubuntu1404
|
||||
image: mesosphere/mesos-master:0.28.1-2.0.20.ubuntu1404
|
||||
net: host
|
||||
environment:
|
||||
MESOS_ZK: zk://127.0.0.1:2181/mesos
|
||||
@@ -17,7 +17,7 @@ master:
|
||||
MESOS_WORK_DIR: /var/lib/mesos
|
||||
|
||||
slave:
|
||||
image: mesosphere/mesos-slave:0.26.0-0.2.145.ubuntu1404
|
||||
image: mesosphere/mesos-slave:0.28.1-2.0.20.ubuntu1404
|
||||
net: host
|
||||
pid: host
|
||||
privileged: true
|
||||
@@ -34,10 +34,19 @@ slave:
|
||||
- /lib/x86_64-linux-gnu/libsystemd-journal.so.0:/lib/x86_64-linux-gnu/libsystemd-journal.so.0
|
||||
|
||||
marathon:
|
||||
image: mesosphere/marathon:v0.13.0
|
||||
image: mesosphere/marathon:v1.1.1
|
||||
net: host
|
||||
environment:
|
||||
MARATHON_MASTER: zk://127.0.0.1:2181/mesos
|
||||
MARATHON_ZK: zk://127.0.0.1:2181/marathon
|
||||
MARATHON_HOSTNAME: 127.0.0.1
|
||||
command: --event_subscriber http_callback
|
||||
|
||||
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
|
||||
ports:
|
||||
- "8000:80"
|
||||
- "8081:8080"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
20
examples/compose-traefik.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
traefik:
|
||||
image: traefik
|
||||
command: -c /dev/null --web --docker --docker.domain=docker.localhost --logLevel=DEBUG
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
whoami1:
|
||||
image: emilevauge/whoami
|
||||
labels:
|
||||
- "traefik.backend=whoami"
|
||||
- "traefik.frontend.rule=Host:whoami.docker.localhost"
|
||||
|
||||
whoami2:
|
||||
image: emilevauge/whoami
|
||||
labels:
|
||||
- "traefik.backend=whoami"
|
||||
- "traefik.frontend.rule=Host:whoami.docker.localhost"
|
@@ -17,11 +17,9 @@ curl -i -H "Accept: application/json" -X PUT -d "2" ht
|
||||
# frontend 1
|
||||
curl -i -H "Accept: application/json" -X PUT -d "backend2" http://localhost:8500/v1/kv/traefik/frontends/frontend1/backend
|
||||
curl -i -H "Accept: application/json" -X PUT -d "http" http://localhost:8500/v1/kv/traefik/frontends/frontend1/entrypoints
|
||||
curl -i -H "Accept: application/json" -X PUT -d "Host" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/rule
|
||||
curl -i -H "Accept: application/json" -X PUT -d "test.localhost" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/value
|
||||
curl -i -H "Accept: application/json" -X PUT -d "Host:test.localhost" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/rule
|
||||
|
||||
# frontend 2
|
||||
curl -i -H "Accept: application/json" -X PUT -d "backend1" http://localhost:8500/v1/kv/traefik/frontends/frontend2/backend
|
||||
curl -i -H "Accept: application/json" -X PUT -d "http,https" http://localhost:8500/v1/kv/traefik/frontends/frontend2/entrypoints
|
||||
curl -i -H "Accept: application/json" -X PUT -d "Path" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/rule
|
||||
curl -i -H "Accept: application/json" -X PUT -d "/test" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/value
|
||||
curl -i -H "Accept: application/json" -X PUT -d "http" http://localhost:8500/v1/kv/traefik/frontends/frontend2/entrypoints
|
||||
curl -i -H "Accept: application/json" -X PUT -d "Path:/test" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/rule
|
25
examples/etcd-config.sh
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
|
||||
# backend 1
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="NetworkErrorRatio() > 0.5" http://localhost:4001/v2/keys/traefik/backends/backend1/circuitbreaker/expression
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="http://172.17.0.2:80" http://localhost:4001/v2/keys/traefik/backends/backend1/servers/server1/url
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="10" http://localhost:4001/v2/keys/traefik/backends/backend1/servers/server1/weight
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="http://172.17.0.3:80" http://localhost:4001/v2/keys/traefik/backends/backend1/servers/server2/url
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="1" http://localhost:4001/v2/keys/traefik/backends/backend1/servers/server2/weight
|
||||
|
||||
# backend 2
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="drr" http://localhost:4001/v2/keys/traefik/backends/backend2/loadbalancer/method
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="http://172.17.0.4:80" http://localhost:4001/v2/keys/traefik/backends/backend2/servers/server1/url
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="1" http://localhost:4001/v2/keys/traefik/backends/backend2/servers/server1/weight
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="http://172.17.0.5:80" http://localhost:4001/v2/keys/traefik/backends/backend2/servers/server2/url
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="2" http://localhost:4001/v2/keys/traefik/backends/backend2/servers/server2/weight
|
||||
|
||||
# frontend 1
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="backend2" http://localhost:4001/v2/keys/traefik/frontends/frontend1/backend
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="http" http://localhost:4001/v2/keys/traefik/frontends/frontend1/entrypoints
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="Host:test.localhost" http://localhost:4001/v2/keys/traefik/frontends/frontend1/routes/test_1/rule
|
||||
|
||||
# frontend 2
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="backend1" http://localhost:4001/v2/keys/traefik/frontends/frontend2/backend
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="http" http://localhost:4001/v2/keys/traefik/frontends/frontend2/entrypoints
|
||||
curl -i -H "Accept: application/json" -X PUT -d value="Path:/test" http://localhost:4001/v2/keys/traefik/frontends/frontend2/routes/test_2/rule
|
93
examples/k8s.ingress.yaml
Normal 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
@@ -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
@@ -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
|
@@ -25,6 +25,7 @@
|
||||
],
|
||||
"labels": {
|
||||
"traefik.weight": "1",
|
||||
"traefik.protocole": "http"
|
||||
"traefik.protocol": "http",
|
||||
"traefik.frontend.rule" : "Host:test.marathon.localhost"
|
||||
}
|
||||
}
|
131
glide.lock
generated
@@ -1,5 +1,5 @@
|
||||
hash: 21d4e8dc80c87101568a719ecf01d1af9a1b58f03c5c9dc864a8cb1005ddc160
|
||||
updated: 2016-03-29T21:50:20.577439177+02:00
|
||||
hash: a9f41b9fe89ac3028da27ac9cbe31db9a79ae89082f42507d4d0c58290517ee2
|
||||
updated: 2016-04-27T17:14:45.61228359Z
|
||||
imports:
|
||||
- name: github.com/alecthomas/template
|
||||
version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0
|
||||
@@ -8,7 +8,7 @@ imports:
|
||||
- name: github.com/boltdb/bolt
|
||||
version: 51f99c862475898df9773747d3accd05a7ca33c1
|
||||
- name: github.com/BurntSushi/toml
|
||||
version: bd2bdf7f18f849530ef7a1c29a4290217cab32a1
|
||||
version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f
|
||||
- name: github.com/BurntSushi/ty
|
||||
version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74
|
||||
subpackages:
|
||||
@@ -22,24 +22,29 @@ imports:
|
||||
- name: github.com/codegangsta/negroni
|
||||
version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b
|
||||
- name: github.com/containous/oxy
|
||||
version: 0b5b371bce661385d35439204298fa6fb5db5463
|
||||
version: 021f82bd8260ba15f5862a9fe62018437720dff5
|
||||
subpackages:
|
||||
- cbreaker
|
||||
- forward
|
||||
- memmetrics
|
||||
- roundrobin
|
||||
- utils
|
||||
- connlimit
|
||||
- stream
|
||||
- name: github.com/coreos/go-etcd
|
||||
version: cc90c7b091275e606ad0ca7102a23fb2072f3f5e
|
||||
- name: github.com/coreos/etcd
|
||||
version: 26e52d2bce9e3e11b77b68cc84bf91aebb1ef637
|
||||
subpackages:
|
||||
- etcd
|
||||
- client
|
||||
- pkg/pathutil
|
||||
- pkg/types
|
||||
- name: github.com/davecgh/go-spew
|
||||
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
|
||||
subpackages:
|
||||
- spew
|
||||
- name: github.com/docker/distribution
|
||||
version: 9038e48c3b982f8e82281ea486f078a73731ac4e
|
||||
version: ff6f38ccb69afa96214c7ee955359465d1fc767a
|
||||
subpackages:
|
||||
- reference
|
||||
- name: github.com/docker/docker
|
||||
version: f39987afe8d611407887b3094c03d6ba6a766a67
|
||||
subpackages:
|
||||
@@ -79,16 +84,33 @@ imports:
|
||||
- runconfig
|
||||
- utils
|
||||
- volume
|
||||
- name: github.com/docker/engine-api
|
||||
version: 8924d6900370b4c7e7984be5adc61f50a80d7537
|
||||
subpackages:
|
||||
- client
|
||||
- types
|
||||
- types/container
|
||||
- types/filters
|
||||
- types/strslice
|
||||
- types/events
|
||||
- client/transport
|
||||
- client/transport/cancellable
|
||||
- types/network
|
||||
- types/registry
|
||||
- types/time
|
||||
- types/blkiodev
|
||||
- name: github.com/docker/go-connections
|
||||
version: f549a9393d05688dff0992ef3efd8bbe6c628aeb
|
||||
subpackages:
|
||||
- nat
|
||||
- sockets
|
||||
- tlsconfig
|
||||
- name: github.com/docker/go-units
|
||||
version: 5d2041e26a699eaca682e2ea41c8f891e1060444
|
||||
- name: github.com/docker/libcompose
|
||||
version: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a
|
||||
subpackages:
|
||||
- docker
|
||||
- logger
|
||||
- lookup
|
||||
- project
|
||||
- utils
|
||||
- name: github.com/docker/libkv
|
||||
version: 3732f7ff1b56057c3158f10bceb1e79133025373
|
||||
version: 7283ef27ed32fe267388510a91709b307bb9942c
|
||||
subpackages:
|
||||
- store
|
||||
- store/boltdb
|
||||
@@ -99,36 +121,26 @@ imports:
|
||||
version: 9cbd2a1374f46905c68a4eb3694a130610adc62a
|
||||
- name: github.com/donovanhide/eventsource
|
||||
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
|
||||
version: d5cac425555ca5cf00694df246e04f05e6a55150
|
||||
- name: github.com/flynn/go-shlex
|
||||
version: 3f9db97f856818214da2e1057f8ad84803971cff
|
||||
- name: github.com/fsouza/go-dockerclient
|
||||
version: a49c8269a6899cae30da1f8a4b82e0ce945f9967
|
||||
subpackages:
|
||||
- external/github.com/docker/docker/opts
|
||||
- external/github.com/docker/docker/pkg/archive
|
||||
- external/github.com/docker/docker/pkg/fileutils
|
||||
- external/github.com/docker/docker/pkg/homedir
|
||||
- external/github.com/docker/docker/pkg/stdcopy
|
||||
- external/github.com/hashicorp/go-cleanhttp
|
||||
- external/github.com/Sirupsen/logrus
|
||||
- external/github.com/docker/docker/pkg/idtools
|
||||
- external/github.com/docker/docker/pkg/ioutils
|
||||
- external/github.com/docker/docker/pkg/pools
|
||||
- external/github.com/docker/docker/pkg/promise
|
||||
- external/github.com/docker/docker/pkg/system
|
||||
- external/github.com/docker/docker/pkg/longpath
|
||||
- external/github.com/opencontainers/runc/libcontainer/user
|
||||
- external/golang.org/x/sys/unix
|
||||
- external/golang.org/x/net/context
|
||||
- external/github.com/docker/go-units
|
||||
- name: github.com/gambol99/go-marathon
|
||||
version: ade11d1dc2884ee1f387078fc28509559b6235d1
|
||||
- name: github.com/go-check/check
|
||||
version: 11d3bc7aa68e238947792f30573146a3231fc0f1
|
||||
- name: github.com/golang/glog
|
||||
version: fca8c8854093a154ff1eb580aae10276ad6b1b5f
|
||||
- name: github.com/golang/snappy
|
||||
version: ec642410cd033af63620b66a91ccbd3c69c2c59a
|
||||
- name: github.com/google/go-querystring
|
||||
version: 6bb77fe6f42b85397288d4f6f67ac72f8f400ee7
|
||||
version: 9235644dd9e52eeae6fa48efd539fdc351a0af53
|
||||
subpackages:
|
||||
- query
|
||||
- name: github.com/gorilla/context
|
||||
@@ -156,6 +168,8 @@ imports:
|
||||
- json/token
|
||||
- name: github.com/inconshreveable/mousetrap
|
||||
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
|
||||
- name: github.com/klauspost/crc32
|
||||
version: 19b0b332c9e4516a6370a0456e6182c3b5036720
|
||||
- name: github.com/kr/pretty
|
||||
version: add1dbc86daf0f983cd4a48ceb39deb95c729b67
|
||||
- name: github.com/kr/text
|
||||
@@ -168,59 +182,73 @@ imports:
|
||||
version: fada45142db3f93097ca917da107aa3fad0ffcb5
|
||||
- name: github.com/mailgun/multibuf
|
||||
version: 565402cd71fbd9c12aa7e295324ea357e970a61e
|
||||
- name: github.com/mailgun/predicate
|
||||
version: cb0bff91a7ab7cf7571e661ff883fc997bc554a3
|
||||
- name: github.com/mailgun/timetools
|
||||
version: fd192d755b00c968d312d23f521eb0cdc6f66bd0
|
||||
- name: github.com/mattn/go-shellwords
|
||||
version: 525bedee691b5a8df547cb5cf9f86b7fb1883e24
|
||||
- name: github.com/Microsoft/go-winio
|
||||
version: 862b6557927a5c5c81e411c12aa6de7e566cbb7a
|
||||
- name: github.com/miekg/dns
|
||||
version: 7e024ce8ce18b21b475ac6baf8fa3c42536bf2fa
|
||||
version: dd83d5cbcfd986f334b2747feeb907e281318fdf
|
||||
- name: github.com/mitchellh/mapstructure
|
||||
version: d2dd0262208475919e1a362f675cfc0e7c10e905
|
||||
- name: github.com/moul/http2curl
|
||||
version: 1812aee76a1ce98d604a44200c6a23c689b17a89
|
||||
- name: github.com/opencontainers/runc
|
||||
version: 4ab132458fc3e9dbeea624153e0331952dc4c8d5
|
||||
subpackages:
|
||||
- libcontainer/user
|
||||
- name: github.com/parnurzeal/gorequest
|
||||
version: 91b42fce877cc6af96c45818665a4c615cc5f4ee
|
||||
- name: github.com/pmezard/go-difflib
|
||||
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
|
||||
subpackages:
|
||||
- difflib
|
||||
- name: github.com/samalba/dockerclient
|
||||
version: cfb489c624b635251a93e74e1e90eb0959c5367f
|
||||
- name: github.com/samuel/go-zookeeper
|
||||
version: fa6674abf3f4580b946a01bf7a1ce4ba8766205b
|
||||
subpackages:
|
||||
- zk
|
||||
- name: github.com/Shopify/sarama
|
||||
version: 92a286e4dde1688175cff3d2ec9b49a02838b447
|
||||
- name: github.com/Sirupsen/logrus
|
||||
version: 418b41d23a1bf978c06faea5313ba194650ac088
|
||||
- name: github.com/spf13/cast
|
||||
version: ee7b3e0353166ab1f3a605294ac8cd2b77953778
|
||||
- name: github.com/spf13/cobra
|
||||
version: c678ff029ee250b65714e518f4f5c5cb934955de
|
||||
version: 4c05eb1145f16d0e6bb4a3e1b6d769f4713cb41f
|
||||
subpackages:
|
||||
- cobra
|
||||
- name: github.com/spf13/jwalterweatherman
|
||||
version: 33c24e77fb80341fe7130ee7c594256ff08ccc46
|
||||
- name: github.com/spf13/pflag
|
||||
version: 7f60f83a2c81bc3c3c0d5297f61ddfa68da9d3b7
|
||||
version: 1f296710f879815ad9e6d39d947c828c3e4b4c3d
|
||||
- name: github.com/spf13/viper
|
||||
version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325
|
||||
- name: github.com/streamrail/concurrent-map
|
||||
version: 788b276dc7eabf20890ea3fa280956664d58b329
|
||||
- name: github.com/stretchr/objx
|
||||
version: cbeaeb16a013161a98496fad62933b1d21786672
|
||||
- name: github.com/stretchr/testify
|
||||
version: 6fe211e493929a8aac0469b93f28b1d0688a9a3a
|
||||
version: bcd9e3389dd03b0b668d11f4d462a6af6c2dfd60
|
||||
subpackages:
|
||||
- mock
|
||||
- assert
|
||||
- name: github.com/thoas/stats
|
||||
version: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8
|
||||
- name: github.com/ugorji/go
|
||||
version: ea9cd21fa0bc41ee4bdd50ac7ed8cbc7ea2ed960
|
||||
subpackages:
|
||||
- codec
|
||||
- name: github.com/unrolled/render
|
||||
version: 26b4e3aac686940fe29521545afad9966ddfc80c
|
||||
- name: github.com/vdemeester/docker-events
|
||||
version: 6ea3f28df37f29a47498bc8b32b36ad8491dbd37
|
||||
- name: github.com/vdemeester/libkermit
|
||||
version: 01a5399bdbd3312916c9fa4848108fbc81fe88d8
|
||||
version: 7e4e689a6fa9281e0fb9b7b9c297e22d5342a5ec
|
||||
- name: github.com/vdemeester/shakers
|
||||
version: 8fe734f75f3a70b651cbfbf8a55a009da09e8dc5
|
||||
version: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3
|
||||
- name: github.com/vulcand/oxy
|
||||
version: 8aaf36279137ac04ace3792a4f86098631b27d5a
|
||||
version: 11677428db34c4a05354d66d028174d0e3c6e905
|
||||
subpackages:
|
||||
- memmetrics
|
||||
- utils
|
||||
@@ -237,11 +265,11 @@ imports:
|
||||
- name: github.com/wendal/errors
|
||||
version: f66c77a7882b399795a8987ebf87ef64a427417e
|
||||
- name: github.com/xenolf/lego
|
||||
version: ca19a90028e242e878585941c2a27c8f3b3efc25
|
||||
version: 23e88185c255e95a106835d80e76e5a3a66d7c54
|
||||
subpackages:
|
||||
- acme
|
||||
- name: golang.org/x/crypto
|
||||
version: 9e7f5dc375abeb9619ea3c5c58502c428f457aa2
|
||||
version: d68c3ecb62c850b645dc072a8d78006286bf81ca
|
||||
subpackages:
|
||||
- ocsp
|
||||
- name: golang.org/x/net
|
||||
@@ -249,14 +277,13 @@ imports:
|
||||
subpackages:
|
||||
- context
|
||||
- publicsuffix
|
||||
- proxy
|
||||
- name: golang.org/x/sys
|
||||
version: eb2c74142fd19a79b3f237334c7384d5167b1b46
|
||||
subpackages:
|
||||
- unix
|
||||
- name: gopkg.in/alecthomas/kingpin.v2
|
||||
version: 639879d6110b1b0409410c7b737ef0bb18325038
|
||||
- name: gopkg.in/check.v1
|
||||
version: 11d3bc7aa68e238947792f30573146a3231fc0f1
|
||||
- name: gopkg.in/fsnotify.v1
|
||||
version: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0
|
||||
- name: gopkg.in/mgo.v2
|
||||
@@ -264,7 +291,7 @@ imports:
|
||||
subpackages:
|
||||
- bson
|
||||
- name: gopkg.in/square/go-jose.v1
|
||||
version: 7d9df93c5ee8a09ed250b3b2360972fa29b4bb3c
|
||||
version: 40d457b439244b546f023d056628e5184136899b
|
||||
subpackages:
|
||||
- cipher
|
||||
- json
|
||||
|
353
glide.yaml
@@ -1,170 +1,187 @@
|
||||
package: main
|
||||
import:
|
||||
- package: github.com/coreos/go-etcd
|
||||
ref: cc90c7b091275e606ad0ca7102a23fb2072f3f5e
|
||||
subpackages:
|
||||
- etcd
|
||||
- package: github.com/docker/distribution
|
||||
ref: 9038e48c3b982f8e82281ea486f078a73731ac4e
|
||||
- package: github.com/mailgun/log
|
||||
ref: 44874009257d4d47ba9806f1b7f72a32a015e4d8
|
||||
- package: github.com/containous/oxy
|
||||
ref: 0b5b371bce661385d35439204298fa6fb5db5463
|
||||
subpackages:
|
||||
- cbreaker
|
||||
- forward
|
||||
- memmetrics
|
||||
- roundrobin
|
||||
- utils
|
||||
- package: github.com/hashicorp/consul
|
||||
ref: de080672fee9e6104572eeea89eccdca135bb918
|
||||
subpackages:
|
||||
- api
|
||||
- package: github.com/samuel/go-zookeeper
|
||||
ref: fa6674abf3f4580b946a01bf7a1ce4ba8766205b
|
||||
subpackages:
|
||||
- zk
|
||||
- package: github.com/docker/libtrust
|
||||
ref: 9cbd2a1374f46905c68a4eb3694a130610adc62a
|
||||
- package: gopkg.in/check.v1
|
||||
ref: 11d3bc7aa68e238947792f30573146a3231fc0f1
|
||||
- package: golang.org/x/net
|
||||
ref: d9558e5c97f85372afee28cf2b6059d7d3818919
|
||||
subpackages:
|
||||
- context
|
||||
- package: github.com/gorilla/handlers
|
||||
ref: 40694b40f4a928c062f56849989d3e9cd0570e5f
|
||||
- package: github.com/docker/libkv
|
||||
ref: 3732f7ff1b56057c3158f10bceb1e79133025373
|
||||
- package: github.com/alecthomas/template
|
||||
ref: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0
|
||||
- package: github.com/vdemeester/shakers
|
||||
ref: 8fe734f75f3a70b651cbfbf8a55a009da09e8dc5
|
||||
- package: github.com/alecthomas/units
|
||||
ref: 6b4e7dc5e3143b85ea77909c72caf89416fc2915
|
||||
- package: github.com/gambol99/go-marathon
|
||||
ref: ade11d1dc2884ee1f387078fc28509559b6235d1
|
||||
- package: github.com/mailgun/predicate
|
||||
ref: cb0bff91a7ab7cf7571e661ff883fc997bc554a3
|
||||
- package: github.com/thoas/stats
|
||||
ref: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8
|
||||
- package: github.com/samalba/dockerclient
|
||||
ref: cfb489c624b635251a93e74e1e90eb0959c5367f
|
||||
- package: github.com/Sirupsen/logrus
|
||||
ref: 418b41d23a1bf978c06faea5313ba194650ac088
|
||||
- package: github.com/unrolled/render
|
||||
ref: 26b4e3aac686940fe29521545afad9966ddfc80c
|
||||
- package: github.com/flynn/go-shlex
|
||||
ref: 3f9db97f856818214da2e1057f8ad84803971cff
|
||||
- package: github.com/fsouza/go-dockerclient
|
||||
ref: a49c8269a6899cae30da1f8a4b82e0ce945f9967
|
||||
- package: github.com/boltdb/bolt
|
||||
ref: 51f99c862475898df9773747d3accd05a7ca33c1
|
||||
- package: gopkg.in/mgo.v2
|
||||
ref: 22287bab4379e1fbf6002fb4eb769888f3fb224c
|
||||
subpackages:
|
||||
- bson
|
||||
- package: github.com/docker/docker
|
||||
ref: f39987afe8d611407887b3094c03d6ba6a766a67
|
||||
subpackages:
|
||||
- autogen
|
||||
- api
|
||||
- cliconfig
|
||||
- daemon/network
|
||||
- graph/tags
|
||||
- image
|
||||
- opts
|
||||
- pkg/archive
|
||||
- pkg/fileutils
|
||||
- pkg/homedir
|
||||
- pkg/httputils
|
||||
- pkg/ioutils
|
||||
- pkg/jsonmessage
|
||||
- pkg/mflag
|
||||
- pkg/nat
|
||||
- pkg/parsers
|
||||
- pkg/pools
|
||||
- pkg/promise
|
||||
- pkg/random
|
||||
- pkg/stdcopy
|
||||
- pkg/stringid
|
||||
- pkg/symlink
|
||||
- pkg/system
|
||||
- pkg/tarsum
|
||||
- pkg/term
|
||||
- pkg/timeutils
|
||||
- pkg/tlsconfig
|
||||
- pkg/ulimit
|
||||
- pkg/units
|
||||
- pkg/urlutil
|
||||
- pkg/useragent
|
||||
- pkg/version
|
||||
- registry
|
||||
- runconfig
|
||||
- utils
|
||||
- volume
|
||||
- package: github.com/mailgun/timetools
|
||||
ref: fd192d755b00c968d312d23f521eb0cdc6f66bd0
|
||||
- package: github.com/codegangsta/negroni
|
||||
ref: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b
|
||||
- package: gopkg.in/yaml.v2
|
||||
ref: 7ad95dd0798a40da1ccdff6dff35fd177b5edf40
|
||||
- package: github.com/opencontainers/runc
|
||||
ref: 4ab132458fc3e9dbeea624153e0331952dc4c8d5
|
||||
subpackages:
|
||||
- libcontainer/user
|
||||
- package: github.com/gorilla/mux
|
||||
ref: f15e0c49460fd49eebe2bcc8486b05d1bef68d3a
|
||||
- package: github.com/BurntSushi/ty
|
||||
ref: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74
|
||||
- package: github.com/elazarl/go-bindata-assetfs
|
||||
ref: d5cac425555ca5cf00694df246e04f05e6a55150
|
||||
- package: github.com/BurntSushi/toml
|
||||
ref: bd2bdf7f18f849530ef7a1c29a4290217cab32a1
|
||||
- package: gopkg.in/alecthomas/kingpin.v2
|
||||
ref: 639879d6110b1b0409410c7b737ef0bb18325038
|
||||
- package: github.com/docker/libcompose
|
||||
ref: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a
|
||||
subpackages:
|
||||
- docker
|
||||
- logger
|
||||
- lookup
|
||||
- project
|
||||
- utils
|
||||
- package: github.com/cenkalti/backoff
|
||||
ref: 4dc77674aceaabba2c7e3da25d4c823edfb73f99
|
||||
- package: gopkg.in/fsnotify.v1
|
||||
ref: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0
|
||||
- package: github.com/mailgun/manners
|
||||
ref: fada45142db3f93097ca917da107aa3fad0ffcb5
|
||||
- package: github.com/gorilla/context
|
||||
ref: 215affda49addc4c8ef7e2534915df2c8c35c6cd
|
||||
- package: github.com/codahale/hdrhistogram
|
||||
ref: 954f16e8b9ef0e5d5189456aa4c1202758e04f17
|
||||
- package: github.com/gorilla/websocket
|
||||
- package: github.com/donovanhide/eventsource
|
||||
ref: d8a3071799b98cacd30b6da92f536050ccfe6da4
|
||||
- package: github.com/golang/glog
|
||||
ref: fca8c8854093a154ff1eb580aae10276ad6b1b5f
|
||||
- package: github.com/spf13/cast
|
||||
ref: ee7b3e0353166ab1f3a605294ac8cd2b77953778
|
||||
- package: github.com/mitchellh/mapstructure
|
||||
- package: github.com/spf13/jwalterweatherman
|
||||
- package: github.com/spf13/pflag
|
||||
- package: github.com/wendal/errors
|
||||
- package: github.com/hashicorp/hcl
|
||||
- package: github.com/kr/pretty
|
||||
- package: github.com/magiconair/properties
|
||||
- package: github.com/kr/text
|
||||
- package: github.com/spf13/viper
|
||||
ref: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325
|
||||
- package: github.com/spf13/cobra
|
||||
subpackages:
|
||||
- /cobra
|
||||
- package: github.com/google/go-querystring/query
|
||||
- package: github.com/vulcand/vulcand/plugin/rewrite
|
||||
- package: github.com/stretchr/testify/mock
|
||||
- package: github.com/xenolf/lego
|
||||
- package: github.com/vdemeester/libkermit
|
||||
ref: 01a5399bdbd3312916c9fa4848108fbc81fe88d8
|
||||
- package: github.com/mailgun/multibuf
|
||||
- package: github.com/coreos/etcd
|
||||
version: 26e52d2bce9e3e11b77b68cc84bf91aebb1ef637
|
||||
subpackages:
|
||||
- client
|
||||
- package: github.com/mailgun/log
|
||||
version: 44874009257d4d47ba9806f1b7f72a32a015e4d8
|
||||
- package: github.com/containous/oxy
|
||||
version: 021f82bd8260ba15f5862a9fe62018437720dff5
|
||||
subpackages:
|
||||
- cbreaker
|
||||
- forward
|
||||
- memmetrics
|
||||
- roundrobin
|
||||
- utils
|
||||
- package: github.com/hashicorp/consul
|
||||
version: de080672fee9e6104572eeea89eccdca135bb918
|
||||
subpackages:
|
||||
- api
|
||||
- package: github.com/samuel/go-zookeeper
|
||||
version: fa6674abf3f4580b946a01bf7a1ce4ba8766205b
|
||||
subpackages:
|
||||
- zk
|
||||
- package: github.com/docker/libtrust
|
||||
version: 9cbd2a1374f46905c68a4eb3694a130610adc62a
|
||||
- package: github.com/go-check/check
|
||||
version: 11d3bc7aa68e238947792f30573146a3231fc0f1
|
||||
- package: golang.org/x/net
|
||||
version: d9558e5c97f85372afee28cf2b6059d7d3818919
|
||||
subpackages:
|
||||
- context
|
||||
- package: github.com/gorilla/handlers
|
||||
version: 40694b40f4a928c062f56849989d3e9cd0570e5f
|
||||
- package: github.com/docker/libkv
|
||||
version: 7283ef27ed32fe267388510a91709b307bb9942c
|
||||
- package: github.com/alecthomas/template
|
||||
version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0
|
||||
- package: github.com/vdemeester/shakers
|
||||
version: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3
|
||||
- package: github.com/alecthomas/units
|
||||
version: 6b4e7dc5e3143b85ea77909c72caf89416fc2915
|
||||
- package: github.com/gambol99/go-marathon
|
||||
version: ade11d1dc2884ee1f387078fc28509559b6235d1
|
||||
- package: github.com/vulcand/predicate
|
||||
version: cb0bff91a7ab7cf7571e661ff883fc997bc554a3
|
||||
- package: github.com/thoas/stats
|
||||
version: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8
|
||||
- package: github.com/Sirupsen/logrus
|
||||
version: 418b41d23a1bf978c06faea5313ba194650ac088
|
||||
- package: github.com/unrolled/render
|
||||
version: 26b4e3aac686940fe29521545afad9966ddfc80c
|
||||
- package: github.com/flynn/go-shlex
|
||||
version: 3f9db97f856818214da2e1057f8ad84803971cff
|
||||
- package: github.com/boltdb/bolt
|
||||
version: 51f99c862475898df9773747d3accd05a7ca33c1
|
||||
- package: gopkg.in/mgo.v2
|
||||
version: 22287bab4379e1fbf6002fb4eb769888f3fb224c
|
||||
subpackages:
|
||||
- bson
|
||||
- package: github.com/docker/docker
|
||||
version: f39987afe8d611407887b3094c03d6ba6a766a67
|
||||
subpackages:
|
||||
- autogen
|
||||
- api
|
||||
- cliconfig
|
||||
- daemon/network
|
||||
- graph/tags
|
||||
- image
|
||||
- opts
|
||||
- pkg/archive
|
||||
- pkg/fileutils
|
||||
- pkg/homedir
|
||||
- pkg/httputils
|
||||
- pkg/ioutils
|
||||
- pkg/jsonmessage
|
||||
- pkg/mflag
|
||||
- pkg/nat
|
||||
- pkg/parsers
|
||||
- pkg/pools
|
||||
- pkg/promise
|
||||
- pkg/random
|
||||
- pkg/stdcopy
|
||||
- pkg/stringid
|
||||
- pkg/symlink
|
||||
- pkg/system
|
||||
- pkg/tarsum
|
||||
- pkg/term
|
||||
- pkg/timeutils
|
||||
- pkg/tlsconfig
|
||||
- pkg/ulimit
|
||||
- pkg/units
|
||||
- pkg/urlutil
|
||||
- pkg/useragent
|
||||
- pkg/version
|
||||
- registry
|
||||
- runconfig
|
||||
- utils
|
||||
- volume
|
||||
- package: github.com/mailgun/timetools
|
||||
version: fd192d755b00c968d312d23f521eb0cdc6f66bd0
|
||||
- package: github.com/codegangsta/negroni
|
||||
version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b
|
||||
- package: gopkg.in/yaml.v2
|
||||
version: 7ad95dd0798a40da1ccdff6dff35fd177b5edf
|
||||
- package: github.com/opencontainers/runc
|
||||
version: 4ab132458fc3e9dbeea624153e0331952dc4c8d5
|
||||
subpackages:
|
||||
- libcontainer/user
|
||||
- package: github.com/gorilla/mux
|
||||
version: f15e0c49460fd49eebe2bcc8486b05d1bef68d3a
|
||||
- package: github.com/BurntSushi/ty
|
||||
version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74
|
||||
- package: github.com/elazarl/go-bindata-assetfs
|
||||
version: d5cac425555ca5cf00694df246e04f05e6a55150
|
||||
- package: github.com/BurntSushi/toml
|
||||
version: bbd5bb678321a0d6e58f1099321dfa73391c1b6f
|
||||
- package: gopkg.in/alecthomas/kingpin.v2
|
||||
version: 639879d6110b1b0409410c7b737ef0bb18325038
|
||||
- package: github.com/cenkalti/backoff
|
||||
version: 4dc77674aceaabba2c7e3da25d4c823edfb73f99
|
||||
- package: gopkg.in/fsnotify.v1
|
||||
version: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0
|
||||
- package: github.com/mailgun/manners
|
||||
version: fada45142db3f93097ca917da107aa3fad0ffcb5
|
||||
- package: github.com/gorilla/context
|
||||
version: 215affda49addc4c8ef7e2534915df2c8c35c6cd
|
||||
- package: github.com/codahale/hdrhistogram
|
||||
version: 954f16e8b9ef0e5d5189456aa4c1202758e04f17
|
||||
- package: github.com/gorilla/websocket
|
||||
- package: github.com/donovanhide/eventsource
|
||||
version: d8a3071799b98cacd30b6da92f536050ccfe6da4
|
||||
- package: github.com/golang/glog
|
||||
version: fca8c8854093a154ff1eb580aae10276ad6b1b5f
|
||||
- package: github.com/spf13/cast
|
||||
version: ee7b3e0353166ab1f3a605294ac8cd2b77953778
|
||||
- package: github.com/mitchellh/mapstructure
|
||||
- package: github.com/spf13/jwalterweatherman
|
||||
- package: github.com/spf13/pflag
|
||||
- package: github.com/wendal/errors
|
||||
- package: github.com/hashicorp/hcl
|
||||
- package: github.com/kr/pretty
|
||||
- package: github.com/magiconair/properties
|
||||
- package: github.com/kr/text
|
||||
- package: github.com/spf13/viper
|
||||
version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325
|
||||
- package: github.com/spf13/cobra
|
||||
subpackages:
|
||||
- cobra
|
||||
- package: github.com/google/go-querystring
|
||||
subpackages:
|
||||
- query
|
||||
- package: github.com/vulcand/vulcand
|
||||
subpackages:
|
||||
- plugin/rewrite
|
||||
- package: github.com/stretchr/testify
|
||||
subpackages:
|
||||
- mock
|
||||
- package: github.com/xenolf/lego
|
||||
- package: github.com/vdemeester/libkermit
|
||||
version: 7e4e689a6fa9281e0fb9b7b9c297e22d5342a5ec
|
||||
- package: github.com/docker/libcompose
|
||||
version: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a
|
||||
- package: github.com/docker/distribution
|
||||
version: ff6f38ccb69afa96214c7ee955359465d1fc767a
|
||||
subpackages:
|
||||
- reference
|
||||
- package: github.com/docker/engine-api
|
||||
version: 8924d6900370b4c7e7984be5adc61f50a80d7537
|
||||
subpackages:
|
||||
- client
|
||||
- types
|
||||
- types/container
|
||||
- types/filters
|
||||
- types/strslice
|
||||
- package: github.com/vdemeester/docker-events
|
||||
- package: github.com/docker/go-connections
|
||||
subpackages:
|
||||
- nat
|
||||
- sockets
|
||||
- tlsconfig
|
||||
- package: github.com/docker/go-units
|
||||
- package: github.com/mailgun/multibuf
|
||||
- package: github.com/streamrail/concurrent-map
|
||||
- package: github.com/parnurzeal/gorequest
|
||||
- package: github.com/mattn/go-shellwords
|
||||
- package: github.com/moul/http2curl
|
||||
|
106
integration/access_log_test.go
Normal 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
|
||||
}
|
@@ -6,8 +6,10 @@ import (
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
"github.com/go-check/check"
|
||||
|
||||
"bytes"
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// SimpleSuite
|
||||
@@ -15,25 +17,45 @@ type SimpleSuite struct{ BaseSuite }
|
||||
|
||||
func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary)
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
c.Assert(err, checker.NotNil)
|
||||
c.Assert(string(output), checker.Contains, "Error reading file: open : no such file or directory")
|
||||
var b bytes.Buffer
|
||||
cmd.Stdout = &b
|
||||
cmd.Stderr = &b
|
||||
|
||||
cmd.Start()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
output := b.Bytes()
|
||||
|
||||
c.Assert(string(output), checker.Contains, "No configuration file found")
|
||||
cmd.Process.Kill()
|
||||
|
||||
nonExistentFile := "non/existent/file.toml"
|
||||
cmd = exec.Command(traefikBinary, "--configFile="+nonExistentFile)
|
||||
output, err = cmd.CombinedOutput()
|
||||
|
||||
c.Assert(err, checker.NotNil)
|
||||
c.Assert(string(output), checker.Contains, fmt.Sprintf("Error reading file: open %s: no such file or directory", nonExistentFile))
|
||||
cmd.Stdout = &b
|
||||
cmd.Stderr = &b
|
||||
|
||||
cmd.Start()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
output = b.Bytes()
|
||||
|
||||
c.Assert(string(output), checker.Contains, fmt.Sprintf("Error reading configuration file: open %s: no such file or directory", nonExistentFile))
|
||||
cmd.Process.Kill()
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestInvalidConfigShouldFail(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/invalid_configuration.toml")
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
c.Assert(err, checker.NotNil)
|
||||
c.Assert(string(output), checker.Contains, "Error reading file: While parsing config: Near line 1")
|
||||
var b bytes.Buffer
|
||||
cmd.Stdout = &b
|
||||
cmd.Stderr = &b
|
||||
|
||||
cmd.Start()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
defer cmd.Process.Kill()
|
||||
output := b.Bytes()
|
||||
|
||||
c.Assert(string(output), checker.Contains, "While parsing config: Near line 0 (last key parsed ''): Bare keys cannot contain '{'")
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestSimpleDefaultConfig(c *check.C) {
|
||||
|
@@ -6,11 +6,10 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
"github.com/hashicorp/consul/api"
|
||||
docker "github.com/vdemeester/libkermit/docker"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Consul catalog test suites
|
||||
@@ -18,20 +17,14 @@ type ConsulCatalogSuite struct {
|
||||
BaseSuite
|
||||
consulIP string
|
||||
consulClient *api.Client
|
||||
project *docker.Project
|
||||
}
|
||||
|
||||
func (s *ConsulCatalogSuite) SetUpSuite(c *check.C) {
|
||||
project, err := docker.NewProjectFromEnv()
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error while creating docker project"))
|
||||
s.project = project
|
||||
|
||||
s.createComposeProject(c, "consul_catalog")
|
||||
err = s.composeProject.Start()
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error starting project"))
|
||||
s.composeProject.Start(c)
|
||||
|
||||
consul, err := s.project.Inspect("integration-test-consul_catalog_consul_1")
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error finding consul container"))
|
||||
consul := s.composeProject.Container(c, "consul")
|
||||
|
||||
s.consulIP = consul.NetworkSettings.IPAddress
|
||||
config := api.DefaultConfig()
|
||||
@@ -46,7 +39,7 @@ func (s *ConsulCatalogSuite) SetUpSuite(c *check.C) {
|
||||
time.Sleep(2000 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (s *ConsulCatalogSuite) registerService(name string, address string, port int) error {
|
||||
func (s *ConsulCatalogSuite) registerService(name string, address string, port int, tags []string) error {
|
||||
catalog := s.consulClient.Catalog()
|
||||
_, err := catalog.Register(
|
||||
&api.CatalogRegistration{
|
||||
@@ -57,6 +50,7 @@ func (s *ConsulCatalogSuite) registerService(name string, address string, port i
|
||||
Service: name,
|
||||
Address: address,
|
||||
Port: port,
|
||||
Tags: tags,
|
||||
},
|
||||
},
|
||||
&api.WriteOptions{},
|
||||
@@ -98,10 +92,9 @@ func (s *ConsulCatalogSuite) TestSingleService(c *check.C) {
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
nginx, err := s.project.Inspect("integration-test-consul_catalog_nginx_1")
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error finding nginx container"))
|
||||
nginx := s.composeProject.Container(c, "nginx")
|
||||
|
||||
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80)
|
||||
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{})
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
|
||||
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress)
|
||||
|
||||
|
@@ -5,28 +5,186 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/docker/libkv"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/consul"
|
||||
"github.com/go-check/check"
|
||||
|
||||
"errors"
|
||||
"github.com/containous/traefik/integration/utils"
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Consul test suites (using libcompose)
|
||||
type ConsulSuite struct{ BaseSuite }
|
||||
type ConsulSuite struct {
|
||||
BaseSuite
|
||||
kv store.Store
|
||||
}
|
||||
|
||||
func (s *ConsulSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "consul")
|
||||
s.composeProject.Start(c)
|
||||
|
||||
consul.Register()
|
||||
kv, err := libkv.NewStore(
|
||||
store.CONSUL,
|
||||
[]string{s.composeProject.Container(c, "consul").NetworkSettings.IPAddress + ":8500"},
|
||||
&store.Config{
|
||||
ConnectionTimeout: 10 * time.Second,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
c.Fatal("Cannot create store consul")
|
||||
}
|
||||
s.kv = kv
|
||||
|
||||
// wait for consul
|
||||
err = utils.Try(60*time.Second, func() error {
|
||||
_, err := kv.Exists("test")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
|
||||
func (s *ConsulSuite) TestSimpleConfiguration(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/consul/simple.toml")
|
||||
consulHost := s.composeProject.Container(c, "consul").NetworkSettings.IPAddress
|
||||
file := s.adaptFile(c, "fixtures/consul/simple.toml", struct{ ConsulHost string }{consulHost})
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
// TODO validate : run on 80
|
||||
resp, err := http.Get("http://127.0.0.1:8000/")
|
||||
|
||||
// Expected a 404 as we did not configure anything
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 404)
|
||||
}
|
||||
|
||||
func (s *ConsulSuite) TestNominalConfiguration(c *check.C) {
|
||||
consulHost := s.composeProject.Container(c, "consul").NetworkSettings.IPAddress
|
||||
file := s.adaptFile(c, "fixtures/consul/simple.toml", struct{ ConsulHost string }{consulHost})
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
whoami1 := s.composeProject.Container(c, "whoami1")
|
||||
whoami2 := s.composeProject.Container(c, "whoami2")
|
||||
whoami3 := s.composeProject.Container(c, "whoami3")
|
||||
whoami4 := s.composeProject.Container(c, "whoami4")
|
||||
|
||||
backend1 := map[string]string{
|
||||
"traefik/backends/backend1/circuitbreaker/expression": "NetworkErrorRatio() > 0.5",
|
||||
"traefik/backends/backend1/servers/server1/url": "http://" + whoami1.NetworkSettings.IPAddress + ":80",
|
||||
"traefik/backends/backend1/servers/server1/weight": "10",
|
||||
"traefik/backends/backend1/servers/server2/url": "http://" + whoami2.NetworkSettings.IPAddress + ":80",
|
||||
"traefik/backends/backend1/servers/server2/weight": "1",
|
||||
}
|
||||
backend2 := map[string]string{
|
||||
"traefik/backends/backend2/loadbalancer/method": "drr",
|
||||
"traefik/backends/backend2/servers/server1/url": "http://" + whoami3.NetworkSettings.IPAddress + ":80",
|
||||
"traefik/backends/backend2/servers/server1/weight": "1",
|
||||
"traefik/backends/backend2/servers/server2/url": "http://" + whoami4.NetworkSettings.IPAddress + ":80",
|
||||
"traefik/backends/backend2/servers/server2/weight": "2",
|
||||
}
|
||||
frontend1 := map[string]string{
|
||||
"traefik/frontends/frontend1/backend": "backend2",
|
||||
"traefik/frontends/frontend1/entrypoints": "http",
|
||||
"traefik/frontends/frontend1/routes/test_1/rule": "Host:test.localhost",
|
||||
}
|
||||
frontend2 := map[string]string{
|
||||
"traefik/frontends/frontend2/backend": "backend1",
|
||||
"traefik/frontends/frontend2/entrypoints": "http",
|
||||
"traefik/frontends/frontend2/routes/test_2/rule": "Path:/test",
|
||||
}
|
||||
for key, value := range backend1 {
|
||||
err := s.kv.Put(key, []byte(value), nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
for key, value := range backend2 {
|
||||
err := s.kv.Put(key, []byte(value), nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
for key, value := range frontend1 {
|
||||
err := s.kv.Put(key, []byte(value), nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
for key, value := range frontend2 {
|
||||
err := s.kv.Put(key, []byte(value), nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
|
||||
// wait for consul
|
||||
err = utils.Try(60*time.Second, func() error {
|
||||
_, err := s.kv.Exists("traefik/frontends/frontend2/routes/test_2/rule")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
// wait for traefik
|
||||
err = utils.TryRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, func(res *http.Response) error {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.Contains(string(body), "Path:/test") {
|
||||
return errors.New("Incorrect traefik config")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
req.Host = "test.localhost"
|
||||
response, err := client.Do(req)
|
||||
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(response.StatusCode, checker.Equals, 200)
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
c.Assert(err, checker.IsNil)
|
||||
if !strings.Contains(string(body), whoami3.NetworkSettings.IPAddress) &&
|
||||
!strings.Contains(string(body), whoami4.NetworkSettings.IPAddress) {
|
||||
c.Fail()
|
||||
}
|
||||
|
||||
req, err = http.NewRequest("GET", "http://127.0.0.1:8000/test", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
response, err = client.Do(req)
|
||||
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(response.StatusCode, checker.Equals, 200)
|
||||
|
||||
body, err = ioutil.ReadAll(response.Body)
|
||||
c.Assert(err, checker.IsNil)
|
||||
if !strings.Contains(string(body), whoami1.NetworkSettings.IPAddress) &&
|
||||
!strings.Contains(string(body), whoami2.NetworkSettings.IPAddress) {
|
||||
c.Fail()
|
||||
}
|
||||
|
||||
req, err = http.NewRequest("GET", "http://127.0.0.1:8000/test2", nil)
|
||||
resp, err := client.Do(req)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 404)
|
||||
|
||||
req, err = http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
|
||||
req.Host = "test2.localhost"
|
||||
resp, err = client.Do(req)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 404)
|
||||
}
|
||||
|
@@ -11,10 +11,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/pkg/namesgenerator"
|
||||
"github.com/vdemeester/libkermit/docker"
|
||||
"github.com/go-check/check"
|
||||
|
||||
d "github.com/vdemeester/libkermit/docker"
|
||||
docker "github.com/vdemeester/libkermit/docker/check"
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -36,46 +37,42 @@ type DockerSuite struct {
|
||||
}
|
||||
|
||||
func (s *DockerSuite) startContainer(c *check.C, image string, args ...string) string {
|
||||
return s.startContainerWithConfig(c, image, docker.ContainerConfig{
|
||||
return s.startContainerWithConfig(c, image, d.ContainerConfig{
|
||||
Cmd: args,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DockerSuite) startContainerWithLabels(c *check.C, image string, labels map[string]string, args ...string) string {
|
||||
return s.startContainerWithConfig(c, image, docker.ContainerConfig{
|
||||
return s.startContainerWithConfig(c, image, d.ContainerConfig{
|
||||
Cmd: args,
|
||||
Labels: labels,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DockerSuite) startContainerWithConfig(c *check.C, image string, config docker.ContainerConfig) string {
|
||||
func (s *DockerSuite) startContainerWithConfig(c *check.C, image string, config d.ContainerConfig) string {
|
||||
if config.Name == "" {
|
||||
config.Name = namesgenerator.GetRandomName(10)
|
||||
}
|
||||
|
||||
container, err := s.project.StartWithConfig(image, config)
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error starting a container using config %v", config))
|
||||
container := s.project.StartWithConfig(c, image, config)
|
||||
|
||||
// FIXME(vdemeester) this is ugly (it's because of the / in front of the name in docker..)
|
||||
return strings.SplitAfter(container.Name, "/")[1]
|
||||
}
|
||||
|
||||
func (s *DockerSuite) SetUpSuite(c *check.C) {
|
||||
project, err := docker.NewProjectFromEnv()
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error while creating docker project"))
|
||||
project := docker.NewProjectFromEnv(c)
|
||||
s.project = project
|
||||
|
||||
// Pull required images
|
||||
for repository, tag := range RequiredImages {
|
||||
image := fmt.Sprintf("%s:%s", repository, tag)
|
||||
s.project.Pull(image)
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error while pulling image %s", image))
|
||||
s.project.Pull(c, image)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DockerSuite) TearDownTest(c *check.C) {
|
||||
err := s.project.Clean(os.Getenv("CIRCLECI") != "")
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error while cleaning containers"))
|
||||
s.project.Clean(c, os.Getenv("CIRCLECI") != "")
|
||||
}
|
||||
|
||||
func (s *DockerSuite) TestSimpleConfiguration(c *check.C) {
|
||||
@@ -133,8 +130,7 @@ func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) {
|
||||
defer os.Remove(file)
|
||||
// Start a container with some labels
|
||||
labels := map[string]string{
|
||||
"traefik.frontend.rule": "Host",
|
||||
"traefik.frontend.value": "my.super.host",
|
||||
"traefik.frontend.rule": "Host:my.super.host",
|
||||
}
|
||||
s.startContainerWithLabels(c, "swarm:1.0.0", labels, "manage", "token://blabla")
|
||||
|
||||
|
@@ -5,19 +5,61 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/containous/traefik/integration/utils"
|
||||
"github.com/docker/libkv"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/etcd"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Etcd test suites (using libcompose)
|
||||
type EtcdSuite struct{ BaseSuite }
|
||||
type EtcdSuite struct {
|
||||
BaseSuite
|
||||
kv store.Store
|
||||
}
|
||||
|
||||
func (s *EtcdSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "etcd")
|
||||
s.composeProject.Start(c)
|
||||
|
||||
etcd.Register()
|
||||
url := s.composeProject.Container(c, "etcd").NetworkSettings.IPAddress + ":4001"
|
||||
kv, err := libkv.NewStore(
|
||||
store.ETCD,
|
||||
[]string{url},
|
||||
&store.Config{
|
||||
ConnectionTimeout: 10 * time.Second,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
c.Fatal("Cannot create store etcd")
|
||||
}
|
||||
s.kv = kv
|
||||
|
||||
// wait for etcd
|
||||
err = utils.Try(60*time.Second, func() error {
|
||||
_, err := kv.Exists("test")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Etcd connection error to %s: %v", url, err)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
|
||||
func (s *EtcdSuite) TestSimpleConfiguration(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/etcd/simple.toml")
|
||||
etcdHost := s.composeProject.Container(c, "etcd").NetworkSettings.IPAddress
|
||||
file := s.adaptFile(c, "fixtures/etcd/simple.toml", struct{ EtcdHost string }{etcdHost})
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
@@ -30,3 +72,123 @@ func (s *EtcdSuite) TestSimpleConfiguration(c *check.C) {
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 404)
|
||||
}
|
||||
|
||||
func (s *EtcdSuite) TestNominalConfiguration(c *check.C) {
|
||||
etcdHost := s.composeProject.Container(c, "etcd").NetworkSettings.IPAddress
|
||||
file := s.adaptFile(c, "fixtures/etcd/simple.toml", struct{ EtcdHost string }{etcdHost})
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
whoami1 := s.composeProject.Container(c, "whoami1")
|
||||
whoami2 := s.composeProject.Container(c, "whoami2")
|
||||
whoami3 := s.composeProject.Container(c, "whoami3")
|
||||
whoami4 := s.composeProject.Container(c, "whoami4")
|
||||
|
||||
backend1 := map[string]string{
|
||||
"/traefik/backends/backend1/circuitbreaker/expression": "NetworkErrorRatio() > 0.5",
|
||||
"/traefik/backends/backend1/servers/server1/url": "http://" + whoami1.NetworkSettings.IPAddress + ":80",
|
||||
"/traefik/backends/backend1/servers/server1/weight": "10",
|
||||
"/traefik/backends/backend1/servers/server2/url": "http://" + whoami2.NetworkSettings.IPAddress + ":80",
|
||||
"/traefik/backends/backend1/servers/server2/weight": "1",
|
||||
}
|
||||
backend2 := map[string]string{
|
||||
"/traefik/backends/backend2/loadbalancer/method": "drr",
|
||||
"/traefik/backends/backend2/servers/server1/url": "http://" + whoami3.NetworkSettings.IPAddress + ":80",
|
||||
"/traefik/backends/backend2/servers/server1/weight": "1",
|
||||
"/traefik/backends/backend2/servers/server2/url": "http://" + whoami4.NetworkSettings.IPAddress + ":80",
|
||||
"/traefik/backends/backend2/servers/server2/weight": "2",
|
||||
}
|
||||
frontend1 := map[string]string{
|
||||
"/traefik/frontends/frontend1/backend": "backend2",
|
||||
"/traefik/frontends/frontend1/entrypoints": "http",
|
||||
"/traefik/frontends/frontend1/routes/test_1/rule": "Host:test.localhost",
|
||||
}
|
||||
frontend2 := map[string]string{
|
||||
"/traefik/frontends/frontend2/backend": "backend1",
|
||||
"/traefik/frontends/frontend2/entrypoints": "http",
|
||||
"/traefik/frontends/frontend2/routes/test_2/rule": "Path:/test",
|
||||
}
|
||||
for key, value := range backend1 {
|
||||
err := s.kv.Put(key, []byte(value), nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
for key, value := range backend2 {
|
||||
err := s.kv.Put(key, []byte(value), nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
for key, value := range frontend1 {
|
||||
err := s.kv.Put(key, []byte(value), nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
for key, value := range frontend2 {
|
||||
err := s.kv.Put(key, []byte(value), nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
|
||||
// wait for etcd
|
||||
err = utils.Try(60*time.Second, func() error {
|
||||
_, err := s.kv.Exists("/traefik/frontends/frontend2/routes/test_2/rule")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
// wait for traefik
|
||||
err = utils.TryRequest("http://127.0.0.1:8081/api/providers", 60*time.Second, func(res *http.Response) error {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.Contains(string(body), "Path:/test") {
|
||||
return errors.New("Incorrect traefik config")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
req.Host = "test.localhost"
|
||||
response, err := client.Do(req)
|
||||
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(response.StatusCode, checker.Equals, 200)
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
c.Assert(err, checker.IsNil)
|
||||
if !strings.Contains(string(body), whoami3.NetworkSettings.IPAddress) &&
|
||||
!strings.Contains(string(body), whoami4.NetworkSettings.IPAddress) {
|
||||
c.Fail()
|
||||
}
|
||||
|
||||
req, err = http.NewRequest("GET", "http://127.0.0.1:8000/test", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
response, err = client.Do(req)
|
||||
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(response.StatusCode, checker.Equals, 200)
|
||||
|
||||
body, err = ioutil.ReadAll(response.Body)
|
||||
c.Assert(err, checker.IsNil)
|
||||
if !strings.Contains(string(body), whoami1.NetworkSettings.IPAddress) &&
|
||||
!strings.Contains(string(body), whoami2.NetworkSettings.IPAddress) {
|
||||
c.Fail()
|
||||
}
|
||||
|
||||
req, err = http.NewRequest("GET", "http://127.0.0.1:8000/test2", nil)
|
||||
req.Host = "test2.localhost"
|
||||
resp, err := client.Do(req)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 404)
|
||||
|
||||
req, err = http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
|
||||
resp, err = client.Do(req)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 404)
|
||||
}
|
||||
|
@@ -5,8 +5,9 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// File test suites
|
||||
@@ -15,7 +16,7 @@ type FileSuite struct{ BaseSuite }
|
||||
func (s *FileSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "file")
|
||||
|
||||
s.composeProject.Start()
|
||||
s.composeProject.Start(c)
|
||||
}
|
||||
|
||||
func (s *FileSuite) TestSimpleConfiguration(c *check.C) {
|
||||
|
46
integration/fixtures/access_log_config.toml
Normal 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"
|
@@ -1,9 +1,16 @@
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[consul]
|
||||
endpoint = "{{.ConsulHost}}:8500"
|
||||
watch = true
|
||||
prefix = "traefik"
|
||||
|
||||
[web]
|
||||
address = ":8081"
|
@@ -1,11 +1,11 @@
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[docker]
|
||||
|
||||
# It's dynamagic !
|
||||
|
@@ -1,10 +1,16 @@
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[etcd]
|
||||
endpoint = "127.0.0.1:4003,127.0.0.1:4002,127.0.0.1:4001"
|
||||
endpoint = "{{.EtcdHost}}:4001"
|
||||
prefix = "/traefik"
|
||||
watch = true
|
||||
|
||||
[web]
|
||||
address = ":8081"
|
@@ -33,10 +33,8 @@ logLevel = "DEBUG"
|
||||
[frontends.frontend1]
|
||||
backend = "backend2"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host"
|
||||
value = "test.localhost"
|
||||
rule = "Host:test.localhost"
|
||||
[frontends.frontend2]
|
||||
backend = "backend1"
|
||||
[frontends.frontend2.routes.test_2]
|
||||
rule = "Path"
|
||||
value = "/test"
|
||||
rule = "Path:/test"
|
||||
|
@@ -27,10 +27,8 @@ defaultEntryPoints = ["https"]
|
||||
[frontends.frontend1]
|
||||
backend = "backend1"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host"
|
||||
value = "snitest.com"
|
||||
rule = "Host:snitest.com"
|
||||
[frontends.frontend2]
|
||||
backend = "backend2"
|
||||
[frontends.frontend2.routes.test_2]
|
||||
rule = "Host"
|
||||
value = "snitest.org"
|
||||
rule = "Host:snitest.org"
|
||||
|
@@ -8,8 +8,9 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// HTTPSSuite
|
||||
|
@@ -11,10 +11,10 @@ import (
|
||||
"text/template"
|
||||
|
||||
"github.com/containous/traefik/integration/utils"
|
||||
"github.com/vdemeester/libkermit/compose"
|
||||
"github.com/go-check/check"
|
||||
|
||||
compose "github.com/vdemeester/libkermit/compose/check"
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
@@ -23,6 +23,7 @@ func Test(t *testing.T) {
|
||||
|
||||
func init() {
|
||||
check.Suite(&SimpleSuite{})
|
||||
check.Suite(&AccessLogSuite{})
|
||||
check.Suite(&HTTPSSuite{})
|
||||
check.Suite(&FileSuite{})
|
||||
check.Suite(&DockerSuite{})
|
||||
@@ -41,17 +42,14 @@ type BaseSuite struct {
|
||||
func (s *BaseSuite) TearDownSuite(c *check.C) {
|
||||
// shutdown and delete compose project
|
||||
if s.composeProject != nil {
|
||||
err := s.composeProject.Stop()
|
||||
c.Assert(err, checker.IsNil)
|
||||
s.composeProject.Stop(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BaseSuite) createComposeProject(c *check.C, name string) {
|
||||
projectName := fmt.Sprintf("integration-test-%s", name)
|
||||
composeFile := fmt.Sprintf("resources/compose/%s.yml", name)
|
||||
composeProject, err := compose.CreateProject(projectName, composeFile)
|
||||
c.Assert(err, checker.IsNil)
|
||||
s.composeProject = composeProject
|
||||
s.composeProject = compose.CreateProject(c, projectName, composeFile)
|
||||
}
|
||||
|
||||
func (s *BaseSuite) traefikCmd(c *check.C, args ...string) (*exec.Cmd, string) {
|
||||
@@ -66,7 +64,11 @@ func (s *BaseSuite) adaptFileForHost(c *check.C, path string) string {
|
||||
// Default docker socket
|
||||
dockerHost = "unix:///var/run/docker.sock"
|
||||
}
|
||||
tempObjects := struct{ DockerHost string }{dockerHost}
|
||||
return s.adaptFile(c, path, tempObjects)
|
||||
}
|
||||
|
||||
func (s *BaseSuite) adaptFile(c *check.C, path string, tempObjects interface{}) string {
|
||||
// Load file
|
||||
tmpl, err := template.ParseFiles(path)
|
||||
c.Assert(err, checker.IsNil)
|
||||
@@ -76,7 +78,7 @@ func (s *BaseSuite) adaptFileForHost(c *check.C, path string) string {
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer tmpFile.Close()
|
||||
|
||||
err = tmpl.ExecuteTemplate(tmpFile, prefix, struct{ DockerHost string }{dockerHost})
|
||||
err = tmpl.ExecuteTemplate(tmpFile, prefix, tempObjects)
|
||||
c.Assert(err, checker.IsNil)
|
||||
err = tmpFile.Sync()
|
||||
|
||||
|
@@ -5,8 +5,9 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Marathon test suites (using libcompose)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
consul:
|
||||
image: progrium/consul
|
||||
command: -server -bootstrap -advertise 12.0.0.254 -log-level debug -ui-dir /ui
|
||||
command: -server -bootstrap -log-level debug -ui-dir /ui
|
||||
ports:
|
||||
- "8400:8400"
|
||||
- "8500:8500"
|
||||
@@ -10,4 +10,16 @@ consul:
|
||||
- "8301"
|
||||
- "8301/udp"
|
||||
- "8302"
|
||||
- "8302/udp"
|
||||
- "8302/udp"
|
||||
|
||||
whoami1:
|
||||
image: emilevauge/whoami
|
||||
|
||||
whoami2:
|
||||
image: emilevauge/whoami
|
||||
|
||||
whoami3:
|
||||
image: emilevauge/whoami
|
||||
|
||||
whoami4:
|
||||
image: emilevauge/whoami
|
@@ -1,30 +1,14 @@
|
||||
etcd1:
|
||||
image: quay.io/coreos/etcd:v2.2.0
|
||||
net: "host"
|
||||
command: >
|
||||
--name etcd1
|
||||
--listen-peer-urls http://localhost:7001
|
||||
--listen-client-urls http://localhost:4001
|
||||
--initial-advertise-peer-urls http://localhost:7001
|
||||
--advertise-client-urls http://localhost:4001
|
||||
--initial-cluster etcd1=http://localhost:7001,etcd2=http://localhost:7002,etcd3=http://localhost:7003
|
||||
etcd2:
|
||||
image: quay.io/coreos/etcd:v2.2.0
|
||||
net: "host"
|
||||
command: >
|
||||
--name etcd2
|
||||
--listen-peer-urls http://localhost:7002
|
||||
--listen-client-urls http://localhost:4002
|
||||
--initial-advertise-peer-urls http://localhost:7002
|
||||
--advertise-client-urls http://localhost:4002
|
||||
--initial-cluster etcd1=http://localhost:7001,etcd2=http://localhost:7002,etcd3=http://localhost:7003
|
||||
etcd3:
|
||||
image: quay.io/coreos/etcd:v2.2.0
|
||||
net: "host"
|
||||
command: >
|
||||
--name etcd3
|
||||
--listen-peer-urls http://localhost:7003
|
||||
--listen-client-urls http://localhost:4003
|
||||
--initial-advertise-peer-urls http://localhost:7003
|
||||
--advertise-client-urls http://localhost:4003
|
||||
--initial-cluster etcd1=http://localhost:7001,etcd2=http://localhost:7002,etcd3=http://localhost:7003
|
||||
etcd:
|
||||
image: containous/docker-etcd
|
||||
|
||||
whoami1:
|
||||
image: emilevauge/whoami
|
||||
|
||||
whoami2:
|
||||
image: emilevauge/whoami
|
||||
|
||||
whoami3:
|
||||
image: emilevauge/whoami
|
||||
|
||||
whoami4:
|
||||
image: emilevauge/whoami
|
50
integration/utils/try.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/cenkalti/backoff"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TryRequest try operation timeout, and retry backoff
|
||||
func TryRequest(url string, timeout time.Duration, condition Condition) error {
|
||||
exponentialBackOff := backoff.NewExponentialBackOff()
|
||||
exponentialBackOff.MaxElapsedTime = timeout
|
||||
var res *http.Response
|
||||
err := backoff.Retry(func() error {
|
||||
var err error
|
||||
res, err = http.Get(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return condition(res)
|
||||
}, exponentialBackOff)
|
||||
return err
|
||||
}
|
||||
|
||||
// Try try operation timeout, and retry backoff
|
||||
func Try(timeout time.Duration, operation func() error) error {
|
||||
exponentialBackOff := backoff.NewExponentialBackOff()
|
||||
exponentialBackOff.MaxElapsedTime = timeout
|
||||
err := backoff.Retry(operation, exponentialBackOff)
|
||||
return err
|
||||
}
|
||||
|
||||
// Condition is a retry condition function.
|
||||
// It receives a response, and returns an error
|
||||
// if the response failed the condition.
|
||||
type Condition func(*http.Response) error
|
||||
|
||||
// ErrorIfStatusCodeIsNot returns a retry condition function.
|
||||
// The condition returns an error
|
||||
// if the given response's status code is not the given HTTP status code.
|
||||
func ErrorIfStatusCodeIsNot(status int) Condition {
|
||||
return func(res *http.Response) error {
|
||||
if res.StatusCode != status {
|
||||
return errors.New("Bad status. Got: " + res.Status + ", expected:" + strconv.Itoa(status))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StripPrefix is a middleware used to strip prefix from an URL request
|
||||
type StripPrefix struct {
|
||||
Handler http.Handler
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if p := strings.TrimPrefix(r.URL.Path, s.Prefix); len(p) < len(r.URL.Path) {
|
||||
r.URL.Path = p
|
||||
r.RequestURI = r.URL.RequestURI()
|
||||
s.Handler.ServeHTTP(w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
@@ -1,40 +1,35 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// HandlerSwitcher allows hot switching of http.ServeMux
|
||||
type HandlerSwitcher struct {
|
||||
handler *mux.Router
|
||||
handlerLock *sync.Mutex
|
||||
handler *safe.Safe
|
||||
}
|
||||
|
||||
// NewHandlerSwitcher builds a new instance of HandlerSwitcher
|
||||
func NewHandlerSwitcher(newHandler *mux.Router) (hs *HandlerSwitcher) {
|
||||
return &HandlerSwitcher{
|
||||
handler: newHandler,
|
||||
handlerLock: &sync.Mutex{},
|
||||
handler: safe.New(newHandler),
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *HandlerSwitcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
hs.handlerLock.Lock()
|
||||
handlerBackup := hs.handler
|
||||
hs.handlerLock.Unlock()
|
||||
handlerBackup := hs.handler.Get().(*mux.Router)
|
||||
handlerBackup.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
// GetHandler returns the current http.ServeMux
|
||||
func (hs *HandlerSwitcher) GetHandler() (newHandler *mux.Router) {
|
||||
return hs.handler
|
||||
handler := hs.handler.Get().(*mux.Router)
|
||||
return handler
|
||||
}
|
||||
|
||||
// UpdateHandler safely updates the current http.ServeMux with a new one
|
||||
func (hs *HandlerSwitcher) UpdateHandler(newHandler *mux.Router) {
|
||||
hs.handlerLock.Lock()
|
||||
hs.handler = newHandler
|
||||
defer hs.handlerLock.Unlock()
|
||||
hs.handler.Set(newHandler)
|
||||
}
|
||||
|
@@ -1,18 +1,54 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/streamrail/concurrent-map"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/handlers"
|
||||
"strconv"
|
||||
"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 {
|
||||
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.
|
||||
func NewLogger(file string) *Logger {
|
||||
if len(file) > 0 {
|
||||
@@ -25,15 +61,132 @@ func NewLogger(file string) *Logger {
|
||||
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) {
|
||||
if l.file == nil {
|
||||
next(rw, r)
|
||||
} 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).
|
||||
func (l *Logger) Close() {
|
||||
l.file.Close()
|
||||
// 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() {
|
||||
if l.file != nil {
|
||||
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
@@ -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) {
|
||||
}
|
20
middlewares/saveBackend.go
Normal 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)
|
||||
}
|
29
middlewares/stripPrefix.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StripPrefix is a middleware used to strip prefix from an URL request
|
||||
type StripPrefix struct {
|
||||
Handler http.Handler
|
||||
Prefixes []string
|
||||
}
|
||||
|
||||
func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
for _, prefix := range s.Prefixes {
|
||||
if p := strings.TrimPrefix(r.URL.Path, strings.TrimSpace(prefix)); len(p) < len(r.URL.Path) {
|
||||
r.URL.Path = p
|
||||
r.RequestURI = r.URL.RequestURI()
|
||||
s.Handler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
// SetHandler sets handler
|
||||
func (s *StripPrefix) SetHandler(Handler http.Handler) {
|
||||
s.Handler = Handler
|
||||
}
|
52
mkdocs.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
site_name: Traefik
|
||||
site_description: Traefik Documentation
|
||||
site_author: containo.us
|
||||
site_url: https://docs.traefik.io
|
||||
|
||||
repo_name: 'GitHub'
|
||||
repo_url: 'https://github.com/containous/traefik'
|
||||
|
||||
# Documentation and theme
|
||||
docs_dir: 'docs'
|
||||
theme: united
|
||||
# theme: readthedocs
|
||||
# theme: 'material'
|
||||
# theme: bootstrap
|
||||
|
||||
site_favicon: 'img/traefik.icon.png'
|
||||
|
||||
# Copyright
|
||||
copyright: Copyright (c) 2016 Containous SAS
|
||||
|
||||
# Options
|
||||
extra:
|
||||
# version: 0.2.2
|
||||
logo: img/traefik.logo.png
|
||||
# author:
|
||||
# twitter: traefikproxy
|
||||
palette:
|
||||
primary: 'blue'
|
||||
accent: 'light blue'
|
||||
i18n:
|
||||
prev: 'Previous'
|
||||
next: 'Next'
|
||||
|
||||
markdown_extensions:
|
||||
# - codehilite(css_class=code)
|
||||
- admonition
|
||||
# - toc:
|
||||
# permalink: '##'
|
||||
# - fenced_code
|
||||
|
||||
extra_css:
|
||||
- css/traefik.css
|
||||
|
||||
# Page tree
|
||||
pages:
|
||||
- Getting Started: index.md
|
||||
- Basics: basics.md
|
||||
- traefik.toml: toml.md
|
||||
- User Guide:
|
||||
- 'Configuration examples': 'user-guide/examples.md'
|
||||
- 'Swarm cluster': 'user-guide/swarm.md'
|
||||
- Benchmarks: benchmarks.md
|
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/boltdb"
|
||||
@@ -13,8 +14,8 @@ type BoltDb struct {
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *BoltDb) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
func (provider *BoltDb) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
|
||||
provider.storeType = store.BOLTDB
|
||||
boltdb.Register()
|
||||
return provider.provide(configurationChan)
|
||||
return provider.provide(configurationChan, pool)
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/consul"
|
||||
@@ -13,8 +14,8 @@ type Consul struct {
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Consul) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
func (provider *Consul) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
|
||||
provider.storeType = store.CONSUL
|
||||
consul.Register()
|
||||
return provider.provide(configurationChan)
|
||||
return provider.provide(configurationChan, pool)
|
||||
}
|
||||
|
@@ -2,12 +2,14 @@ package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/hashicorp/consul/api"
|
||||
)
|
||||
@@ -15,6 +17,8 @@ import (
|
||||
const (
|
||||
// DefaultWatchWaitTime is the duration to wait when polling consul
|
||||
DefaultWatchWaitTime = 15 * time.Second
|
||||
// DefaultConsulCatalogTagPrefix is a prefix for additional service/node configurations
|
||||
DefaultConsulCatalogTagPrefix = "traefik"
|
||||
)
|
||||
|
||||
// ConsulCatalog holds configurations of the Consul catalog provider.
|
||||
@@ -23,10 +27,16 @@ type ConsulCatalog struct {
|
||||
Endpoint string
|
||||
Domain string
|
||||
client *api.Client
|
||||
Prefix string
|
||||
}
|
||||
|
||||
type serviceUpdate struct {
|
||||
ServiceName string
|
||||
Attributes []string
|
||||
}
|
||||
|
||||
type catalogUpdate struct {
|
||||
Service string
|
||||
Service *serviceUpdate
|
||||
Nodes []*api.ServiceEntry
|
||||
}
|
||||
|
||||
@@ -35,7 +45,7 @@ func (provider *ConsulCatalog) watchServices(stopCh <-chan struct{}) <-chan map[
|
||||
|
||||
catalog := provider.client.Catalog()
|
||||
|
||||
go func() {
|
||||
safe.Go(func() {
|
||||
defer close(watchCh)
|
||||
|
||||
opts := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
|
||||
@@ -64,7 +74,7 @@ func (provider *ConsulCatalog) watchServices(stopCh <-chan struct{}) <-chan map[
|
||||
watchCh <- data
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
return watchCh
|
||||
}
|
||||
@@ -78,41 +88,102 @@ func (provider *ConsulCatalog) healthyNodes(service string) (catalogUpdate, erro
|
||||
return catalogUpdate{}, err
|
||||
}
|
||||
|
||||
set := map[string]bool{}
|
||||
tags := []string{}
|
||||
for _, node := range data {
|
||||
for _, tag := range node.Service.Tags {
|
||||
if _, ok := set[tag]; ok == false {
|
||||
set[tag] = true
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return catalogUpdate{
|
||||
Service: service,
|
||||
Nodes: data,
|
||||
Service: &serviceUpdate{
|
||||
ServiceName: service,
|
||||
Attributes: tags,
|
||||
},
|
||||
Nodes: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) getEntryPoints(list string) []string {
|
||||
return strings.Split(list, ",")
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) getBackend(node *api.ServiceEntry) string {
|
||||
return strings.ToLower(node.Service.Service)
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) getFrontendValue(service string) string {
|
||||
return service + "." + provider.Domain
|
||||
func (provider *ConsulCatalog) getFrontendRule(service serviceUpdate) string {
|
||||
customFrontendRule := provider.getAttribute("frontend.rule", service.Attributes, "")
|
||||
if customFrontendRule != "" {
|
||||
return customFrontendRule
|
||||
}
|
||||
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 {
|
||||
for _, tag := range tags {
|
||||
if strings.Index(strings.ToLower(tag), DefaultConsulCatalogTagPrefix+".") == 0 {
|
||||
if kv := strings.SplitN(tag[len(DefaultConsulCatalogTagPrefix+"."):], "=", 2); len(kv) == 2 && strings.ToLower(kv[0]) == strings.ToLower(name) {
|
||||
return kv[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) buildConfig(catalog []catalogUpdate) *types.Configuration {
|
||||
var FuncMap = template.FuncMap{
|
||||
"getBackend": provider.getBackend,
|
||||
"getFrontendValue": provider.getFrontendValue,
|
||||
"replace": replace,
|
||||
"getBackend": provider.getBackend,
|
||||
"getFrontendRule": provider.getFrontendRule,
|
||||
"getBackendName": provider.getBackendName,
|
||||
"getBackendAddress": provider.getBackendAddress,
|
||||
"getAttribute": provider.getAttribute,
|
||||
"getEntryPoints": provider.getEntryPoints,
|
||||
}
|
||||
|
||||
allNodes := []*api.ServiceEntry{}
|
||||
serviceNames := []string{}
|
||||
services := []*serviceUpdate{}
|
||||
for _, info := range catalog {
|
||||
if len(info.Nodes) > 0 {
|
||||
serviceNames = append(serviceNames, info.Service)
|
||||
allNodes = append(allNodes, info.Nodes...)
|
||||
for _, node := range info.Nodes {
|
||||
isEnabled := provider.getAttribute("enable", node.Service.Tags, "true")
|
||||
if isEnabled != "false" && len(info.Nodes) > 0 {
|
||||
services = append(services, info.Service)
|
||||
allNodes = append(allNodes, info.Nodes...)
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
templateObjects := struct {
|
||||
Services []string
|
||||
Services []*serviceUpdate
|
||||
Nodes []*api.ServiceEntry
|
||||
}{
|
||||
Services: serviceNames,
|
||||
Services: services,
|
||||
Nodes: allNodes,
|
||||
}
|
||||
|
||||
@@ -145,7 +216,7 @@ func (provider *ConsulCatalog) getNodes(index map[string][]string) ([]catalogUpd
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) watch(configurationChan chan<- types.ConfigMessage) error {
|
||||
func (provider *ConsulCatalog) watch(configurationChan chan<- types.ConfigMessage, stop chan bool) error {
|
||||
stopCh := make(chan struct{})
|
||||
serviceCatalog := provider.watchServices(stopCh)
|
||||
|
||||
@@ -153,6 +224,8 @@ func (provider *ConsulCatalog) watch(configurationChan chan<- types.ConfigMessag
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return nil
|
||||
case index, ok := <-serviceCatalog:
|
||||
if !ok {
|
||||
return errors.New("Consul service list nil")
|
||||
@@ -173,7 +246,7 @@ func (provider *ConsulCatalog) watch(configurationChan chan<- types.ConfigMessag
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *ConsulCatalog) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
func (provider *ConsulCatalog) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
|
||||
config := api.DefaultConfig()
|
||||
config.Address = provider.Endpoint
|
||||
client, err := api.NewClient(config)
|
||||
@@ -182,18 +255,18 @@ func (provider *ConsulCatalog) Provide(configurationChan chan<- types.ConfigMess
|
||||
}
|
||||
provider.client = client
|
||||
|
||||
go func() {
|
||||
pool.Go(func(stop chan bool) {
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Consul connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
worker := func() error {
|
||||
return provider.watch(configurationChan)
|
||||
return provider.watch(configurationChan, stop)
|
||||
}
|
||||
err := backoff.RetryNotify(worker, backoff.NewExponentialBackOff(), notify)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot connect to consul server %+v", err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
@@ -8,23 +8,156 @@ import (
|
||||
"github.com/hashicorp/consul/api"
|
||||
)
|
||||
|
||||
func TestConsulCatalogGetFrontendValue(t *testing.T) {
|
||||
func TestConsulCatalogGetFrontendRule(t *testing.T) {
|
||||
provider := &ConsulCatalog{
|
||||
Domain: "localhost",
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
service string
|
||||
service serviceUpdate
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
service: "foo",
|
||||
expected: "foo.localhost",
|
||||
service: serviceUpdate{
|
||||
ServiceName: "foo",
|
||||
Attributes: []string{},
|
||||
},
|
||||
expected: "Host:foo.localhost",
|
||||
},
|
||||
{
|
||||
service: serviceUpdate{
|
||||
ServiceName: "foo",
|
||||
Attributes: []string{
|
||||
"traefik.frontend.rule=Host:*.example.com",
|
||||
},
|
||||
},
|
||||
expected: "Host:*.example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
actual := provider.getFrontendValue(e.service)
|
||||
actual := provider.getFrontendRule(e.service)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsulCatalogGetAttribute(t *testing.T) {
|
||||
provider := &ConsulCatalog{
|
||||
Domain: "localhost",
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
tags []string
|
||||
key string
|
||||
defaultValue string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
tags: []string{
|
||||
"foo.bar=ramdom",
|
||||
"traefik.backend.weight=42",
|
||||
},
|
||||
key: "backend.weight",
|
||||
defaultValue: "",
|
||||
expected: "42",
|
||||
},
|
||||
{
|
||||
tags: []string{
|
||||
"foo.bar=ramdom",
|
||||
"traefik.backend.wei=42",
|
||||
},
|
||||
key: "backend.weight",
|
||||
defaultValue: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
actual := provider.getAttribute(e.key, e.tags, e.defaultValue)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -49,7 +182,10 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
|
||||
{
|
||||
nodes: []catalogUpdate{
|
||||
{
|
||||
Service: "test",
|
||||
Service: &serviceUpdate{
|
||||
ServiceName: "test",
|
||||
Attributes: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedFrontends: map[string]*types.Frontend{},
|
||||
@@ -58,12 +194,26 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
|
||||
{
|
||||
nodes: []catalogUpdate{
|
||||
{
|
||||
Service: "test",
|
||||
Service: &serviceUpdate{
|
||||
ServiceName: "test",
|
||||
Attributes: []string{
|
||||
"traefik.backend.loadbalancer=drr",
|
||||
"traefik.backend.circuitbreaker=NetworkErrorRatio() > 0.5",
|
||||
"random.foo=bar",
|
||||
},
|
||||
},
|
||||
Nodes: []*api.ServiceEntry{
|
||||
{
|
||||
Service: &api.AgentService{
|
||||
Service: "test",
|
||||
Address: "127.0.0.1",
|
||||
Port: 80,
|
||||
Tags: []string{
|
||||
"traefik.backend.weight=42",
|
||||
"random.foo=bar",
|
||||
"traefik.backend.passHostHeader=true",
|
||||
"traefik.protocol=https",
|
||||
},
|
||||
},
|
||||
Node: &api.Node{
|
||||
Node: "localhost",
|
||||
@@ -78,8 +228,7 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
|
||||
Backend: "backend-test",
|
||||
Routes: map[string]types.Route{
|
||||
"route-host-test": {
|
||||
Rule: "Host",
|
||||
Value: "test.localhost",
|
||||
Rule: "Host:test.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -87,12 +236,17 @@ func TestConsulCatalogBuildConfig(t *testing.T) {
|
||||
expectedBackends: map[string]*types.Backend{
|
||||
"backend-test": {
|
||||
Servers: map[string]types.Server{
|
||||
"server-localhost-80": {
|
||||
URL: "http://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",
|
||||
Weight: 42,
|
||||
},
|
||||
},
|
||||
CircuitBreaker: nil,
|
||||
LoadBalancer: nil,
|
||||
CircuitBreaker: &types.CircuitBreaker{
|
||||
Expression: "NetworkErrorRatio() > 0.5",
|
||||
},
|
||||
LoadBalancer: &types.LoadBalancer{
|
||||
Method: "drr",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@@ -2,19 +2,31 @@ package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/fsouza/go-dockerclient"
|
||||
"github.com/docker/engine-api/client"
|
||||
dockertypes "github.com/docker/engine-api/types"
|
||||
eventtypes "github.com/docker/engine-api/types/events"
|
||||
"github.com/docker/engine-api/types/filters"
|
||||
"github.com/docker/go-connections/sockets"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
"github.com/vdemeester/docker-events"
|
||||
)
|
||||
|
||||
// DockerAPIVersion is a constant holding the version of the Docker API traefik will use
|
||||
const DockerAPIVersion string = "1.21"
|
||||
|
||||
// Docker holds configurations of the Docker provider.
|
||||
type Docker struct {
|
||||
BaseProvider `mapstructure:",squash"`
|
||||
@@ -31,59 +43,105 @@ type DockerTLS struct {
|
||||
InsecureSkipVerify bool
|
||||
}
|
||||
|
||||
func (provider *Docker) createClient() (client.APIClient, error) {
|
||||
var httpClient *http.Client
|
||||
httpHeaders := map[string]string{
|
||||
// FIXME(vdemeester) use version here O:)
|
||||
"User-Agent": "Traefik",
|
||||
}
|
||||
if provider.TLS != nil {
|
||||
tlsOptions := tlsconfig.Options{
|
||||
CAFile: provider.TLS.CA,
|
||||
CertFile: provider.TLS.Cert,
|
||||
KeyFile: provider.TLS.Key,
|
||||
InsecureSkipVerify: provider.TLS.InsecureSkipVerify,
|
||||
}
|
||||
config, err := tlsconfig.Client(tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: config,
|
||||
}
|
||||
proto, addr, _, err := client.ParseHost(provider.Endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sockets.ConfigureTransport(tr, proto, addr)
|
||||
|
||||
httpClient = &http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
}
|
||||
return client.NewClient(provider.Endpoint, DockerAPIVersion, httpClient, httpHeaders)
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
go func() {
|
||||
func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
|
||||
// TODO register this routine in pool, and watch for stop channel
|
||||
safe.Go(func() {
|
||||
operation := func() error {
|
||||
var dockerClient *docker.Client
|
||||
var err error
|
||||
|
||||
if provider.TLS != nil {
|
||||
dockerClient, err = docker.NewTLSClient(provider.Endpoint,
|
||||
provider.TLS.Cert, provider.TLS.Key, provider.TLS.CA)
|
||||
if err == nil {
|
||||
dockerClient.TLSConfig.InsecureSkipVerify = provider.TLS.InsecureSkipVerify
|
||||
}
|
||||
} else {
|
||||
dockerClient, err = docker.NewClient(provider.Endpoint)
|
||||
}
|
||||
dockerClient, err := provider.createClient()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create a client for docker, error: %s", err)
|
||||
return err
|
||||
}
|
||||
err = dockerClient.Ping()
|
||||
version, err := dockerClient.ServerVersion(context.Background())
|
||||
log.Debugf("Docker connection established with docker %s (API %s)", version.Version, version.APIVersion)
|
||||
containers, err := listContainers(dockerClient)
|
||||
if err != nil {
|
||||
log.Errorf("Docker connection error %+v", err)
|
||||
log.Errorf("Failed to list containers for docker, error %s", err)
|
||||
return err
|
||||
}
|
||||
log.Debug("Docker connection established")
|
||||
configuration := provider.loadDockerConfig(listContainers(dockerClient))
|
||||
configuration := provider.loadDockerConfig(containers)
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "docker",
|
||||
Configuration: configuration,
|
||||
}
|
||||
if provider.Watch {
|
||||
dockerEvents := make(chan *docker.APIEvents)
|
||||
dockerClient.AddEventListener(dockerEvents)
|
||||
log.Debug("Docker listening")
|
||||
for {
|
||||
event := <-dockerEvents
|
||||
if event == nil {
|
||||
return errors.New("Docker event nil")
|
||||
// log.Fatalf("Docker connection error")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
f := filters.NewArgs()
|
||||
f.Add("type", "container")
|
||||
options := dockertypes.EventsOptions{
|
||||
Filters: f,
|
||||
}
|
||||
eventHandler := events.NewHandler(events.ByAction)
|
||||
startStopHandle := func(m eventtypes.Message) {
|
||||
log.Debugf("Docker event received %+v", m)
|
||||
containers, err := listContainers(dockerClient)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to list containers for docker, error %s", err)
|
||||
// Call cancel to get out of the monitor
|
||||
cancel()
|
||||
}
|
||||
if event.Status == "start" || event.Status == "die" {
|
||||
log.Debugf("Docker event receveived %+v", event)
|
||||
configuration := provider.loadDockerConfig(listContainers(dockerClient))
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "docker",
|
||||
Configuration: configuration,
|
||||
}
|
||||
configuration := provider.loadDockerConfig(containers)
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "docker",
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
eventHandler.Handle("start", startStopHandle)
|
||||
eventHandler.Handle("die", startStopHandle)
|
||||
|
||||
errChan := events.MonitorWithHandler(ctx, dockerClient, options, eventHandler)
|
||||
pool.Go(func(stop chan bool) {
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
if err := <-errChan; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -94,12 +152,12 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage) er
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot connect to docker server %+v", err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Docker) loadDockerConfig(containersInspected []docker.Container) *types.Configuration {
|
||||
func (provider *Docker) loadDockerConfig(containersInspected []dockertypes.ContainerJSON) *types.Configuration {
|
||||
var DockerFuncMap = template.FuncMap{
|
||||
"getBackend": provider.getBackend,
|
||||
"getPort": provider.getPort,
|
||||
@@ -108,22 +166,21 @@ func (provider *Docker) loadDockerConfig(containersInspected []docker.Container)
|
||||
"getProtocol": provider.getProtocol,
|
||||
"getPassHostHeader": provider.getPassHostHeader,
|
||||
"getEntryPoints": provider.getEntryPoints,
|
||||
"getFrontendValue": provider.getFrontendValue,
|
||||
"getFrontendRule": provider.getFrontendRule,
|
||||
"replace": replace,
|
||||
}
|
||||
|
||||
// filter containers
|
||||
filteredContainers := fun.Filter(containerFilter, containersInspected).([]docker.Container)
|
||||
filteredContainers := fun.Filter(containerFilter, containersInspected).([]dockertypes.ContainerJSON)
|
||||
|
||||
frontends := map[string][]docker.Container{}
|
||||
frontends := map[string][]dockertypes.ContainerJSON{}
|
||||
for _, container := range filteredContainers {
|
||||
frontends[provider.getFrontendName(container)] = append(frontends[provider.getFrontendName(container)], container)
|
||||
}
|
||||
|
||||
templateObjects := struct {
|
||||
Containers []docker.Container
|
||||
Frontends map[string][]docker.Container
|
||||
Containers []dockertypes.ContainerJSON
|
||||
Frontends map[string][]dockertypes.ContainerJSON
|
||||
Domain string
|
||||
}{
|
||||
filteredContainers,
|
||||
@@ -138,7 +195,7 @@ func (provider *Docker) loadDockerConfig(containersInspected []docker.Container)
|
||||
return configuration
|
||||
}
|
||||
|
||||
func containerFilter(container docker.Container) bool {
|
||||
func containerFilter(container dockertypes.ContainerJSON) bool {
|
||||
if len(container.NetworkSettings.Ports) == 0 {
|
||||
log.Debugf("Filtering container without port %s", container.Name)
|
||||
return false
|
||||
@@ -154,50 +211,40 @@ func containerFilter(container docker.Container) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
labels, err := getLabels(container, []string{"traefik.frontend.rule", "traefik.frontend.value"})
|
||||
if len(labels) != 0 && err != nil {
|
||||
log.Debugf("Filtering bad labeled container %s", container.Name)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (provider *Docker) getFrontendName(container docker.Container) string {
|
||||
func (provider *Docker) getFrontendName(container dockertypes.ContainerJSON) string {
|
||||
// Replace '.' with '-' in quoted keys because of this issue https://github.com/BurntSushi/toml/issues/78
|
||||
frontendName := fmt.Sprintf("%s-%s", provider.getFrontendRule(container), provider.getFrontendValue(container))
|
||||
frontendName = strings.Replace(frontendName, "[", "", -1)
|
||||
frontendName = strings.Replace(frontendName, "]", "", -1)
|
||||
|
||||
return strings.Replace(frontendName, ".", "-", -1)
|
||||
}
|
||||
|
||||
// GetFrontendValue returns the frontend value for the specified container, using
|
||||
// it's label. It returns a default one if the label is not present.
|
||||
func (provider *Docker) getFrontendValue(container docker.Container) string {
|
||||
if label, err := getLabel(container, "traefik.frontend.value"); err == nil {
|
||||
return label
|
||||
}
|
||||
return getEscapedName(container.Name) + "." + provider.Domain
|
||||
return normalize(provider.getFrontendRule(container))
|
||||
}
|
||||
|
||||
// GetFrontendRule returns the frontend rule for the specified container, using
|
||||
// it's label. It returns a default one (Host) if the label is not present.
|
||||
func (provider *Docker) getFrontendRule(container docker.Container) string {
|
||||
func (provider *Docker) getFrontendRule(container dockertypes.ContainerJSON) string {
|
||||
// ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
|
||||
// TODO: backwards compatibility with DEPRECATED rule.Value
|
||||
if value, ok := container.Config.Labels["traefik.frontend.value"]; ok {
|
||||
log.Warnf("Label traefik.frontend.value=%s is DEPRECATED (will be removed in v1.0.0), please refer to the rule label: https://github.com/containous/traefik/blob/master/docs/index.md#docker", value)
|
||||
rule, _ := container.Config.Labels["traefik.frontend.rule"]
|
||||
return rule + ":" + value
|
||||
}
|
||||
// ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
|
||||
|
||||
if label, err := getLabel(container, "traefik.frontend.rule"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "Host"
|
||||
return "Host:" + getEscapedName(container.Name) + "." + provider.Domain
|
||||
}
|
||||
|
||||
func (provider *Docker) getBackend(container docker.Container) string {
|
||||
func (provider *Docker) getBackend(container dockertypes.ContainerJSON) string {
|
||||
if label, err := getLabel(container, "traefik.backend"); err == nil {
|
||||
return label
|
||||
}
|
||||
return getEscapedName(container.Name)
|
||||
return normalize(container.Name)
|
||||
}
|
||||
|
||||
func (provider *Docker) getPort(container docker.Container) string {
|
||||
func (provider *Docker) getPort(container dockertypes.ContainerJSON) string {
|
||||
if label, err := getLabel(container, "traefik.port"); err == nil {
|
||||
return label
|
||||
}
|
||||
@@ -207,42 +254,42 @@ func (provider *Docker) getPort(container docker.Container) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (provider *Docker) getWeight(container docker.Container) string {
|
||||
func (provider *Docker) getWeight(container dockertypes.ContainerJSON) string {
|
||||
if label, err := getLabel(container, "traefik.weight"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "0"
|
||||
return "1"
|
||||
}
|
||||
|
||||
func (provider *Docker) getDomain(container docker.Container) string {
|
||||
func (provider *Docker) getDomain(container dockertypes.ContainerJSON) string {
|
||||
if label, err := getLabel(container, "traefik.domain"); err == nil {
|
||||
return label
|
||||
}
|
||||
return provider.Domain
|
||||
}
|
||||
|
||||
func (provider *Docker) getProtocol(container docker.Container) string {
|
||||
func (provider *Docker) getProtocol(container dockertypes.ContainerJSON) string {
|
||||
if label, err := getLabel(container, "traefik.protocol"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (provider *Docker) getPassHostHeader(container docker.Container) string {
|
||||
func (provider *Docker) getPassHostHeader(container dockertypes.ContainerJSON) string {
|
||||
if passHostHeader, err := getLabel(container, "traefik.frontend.passHostHeader"); err == nil {
|
||||
return passHostHeader
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func (provider *Docker) getEntryPoints(container docker.Container) []string {
|
||||
func (provider *Docker) getEntryPoints(container dockertypes.ContainerJSON) []string {
|
||||
if entryPoints, err := getLabel(container, "traefik.frontend.entryPoints"); err == nil {
|
||||
return strings.Split(entryPoints, ",")
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func getLabel(container docker.Container, label string) (string, error) {
|
||||
func getLabel(container dockertypes.ContainerJSON, label string) (string, error) {
|
||||
for key, value := range container.Config.Labels {
|
||||
if key == label {
|
||||
return value, nil
|
||||
@@ -251,7 +298,7 @@ func getLabel(container docker.Container, label string) (string, error) {
|
||||
return "", errors.New("Label not found:" + label)
|
||||
}
|
||||
|
||||
func getLabels(container docker.Container, labels []string) (map[string]string, error) {
|
||||
func getLabels(container dockertypes.ContainerJSON, labels []string) (map[string]string, error) {
|
||||
var globalErr error
|
||||
foundLabels := map[string]string{}
|
||||
for _, label := range labels {
|
||||
@@ -267,14 +314,20 @@ func getLabels(container docker.Container, labels []string) (map[string]string,
|
||||
return foundLabels, globalErr
|
||||
}
|
||||
|
||||
func listContainers(dockerClient *docker.Client) []docker.Container {
|
||||
containerList, _ := dockerClient.ListContainers(docker.ListContainersOptions{})
|
||||
containersInspected := []docker.Container{}
|
||||
func listContainers(dockerClient client.APIClient) ([]dockertypes.ContainerJSON, error) {
|
||||
containerList, err := dockerClient.ContainerList(context.Background(), dockertypes.ContainerListOptions{})
|
||||
if err != nil {
|
||||
return []dockertypes.ContainerJSON{}, err
|
||||
}
|
||||
containersInspected := []dockertypes.ContainerJSON{}
|
||||
|
||||
// get inspect containers
|
||||
for _, container := range containerList {
|
||||
containerInspected, _ := dockerClient.InspectContainer(container.ID)
|
||||
containersInspected = append(containersInspected, *containerInspected)
|
||||
containerInspected, err := dockerClient.ContainerInspect(context.Background(), container.ID)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to inpsect container %s, error: %s", container.ID, err)
|
||||
}
|
||||
containersInspected = append(containersInspected, containerInspected)
|
||||
}
|
||||
return containersInspected
|
||||
return containersInspected, nil
|
||||
}
|
||||
|
@@ -6,7 +6,10 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/fsouza/go-dockerclient"
|
||||
docker "github.com/docker/engine-api/types"
|
||||
"github.com/docker/engine-api/types/container"
|
||||
"github.com/docker/engine-api/types/network"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
func TestDockerGetFrontendName(t *testing.T) {
|
||||
@@ -15,61 +18,69 @@ func TestDockerGetFrontendName(t *testing.T) {
|
||||
}
|
||||
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "foo",
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "Host-foo-docker-localhost",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "bar",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "bar",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Header",
|
||||
"traefik.frontend.rule": "Headers:User-Agent,bat/0.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "Header-bar-docker-localhost",
|
||||
expected: "Headers-User-Agent-bat-0-1-0",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "test",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.value": "foo.bar",
|
||||
"traefik.frontend.rule": "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "Host-foo-bar",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "test",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.value": "foo.bar",
|
||||
"traefik.frontend.rule": "Header",
|
||||
"traefik.frontend.rule": "Path:/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "Header-foo-bar",
|
||||
expected: "Path-test",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "test",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.value": "[foo.bar]",
|
||||
"traefik.frontend.rule": "Header",
|
||||
"traefik.frontend.rule": "PathPrefix:/test2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "Header-foo-bar",
|
||||
expected: "PathPrefix-test2",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -81,74 +92,58 @@ func TestDockerGetFrontendName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerGetFrontendValue(t *testing.T) {
|
||||
func TestDockerGetFrontendRule(t *testing.T) {
|
||||
provider := &Docker{
|
||||
Domain: "docker.localhost",
|
||||
}
|
||||
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "foo",
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "foo.docker.localhost",
|
||||
expected: "Host:foo.docker.localhost",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "bar",
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "bar",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "bar.docker.localhost",
|
||||
expected: "Host:bar.docker.localhost",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "test",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.value": "foo.bar",
|
||||
"traefik.frontend.rule": "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "foo.bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
actual := provider.getFrontendValue(e.container)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerGetFrontendRule(t *testing.T) {
|
||||
provider := &Docker{}
|
||||
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "foo",
|
||||
Config: &docker.Config{},
|
||||
},
|
||||
expected: "Host",
|
||||
expected: "Host:foo.bar",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "test",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "foo",
|
||||
"traefik.frontend.rule": "Path:/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "foo",
|
||||
expected: "Path:/test",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -164,27 +159,33 @@ func TestDockerGetBackend(t *testing.T) {
|
||||
provider := &Docker{}
|
||||
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "foo",
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "foo",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "bar",
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "bar",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "bar",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "test",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.backend": "foobar",
|
||||
},
|
||||
@@ -206,24 +207,30 @@ func TestDockerGetPort(t *testing.T) {
|
||||
provider := &Docker{}
|
||||
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "foo",
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "bar",
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "bar",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -231,9 +238,9 @@ func TestDockerGetPort(t *testing.T) {
|
||||
},
|
||||
// FIXME handle this better..
|
||||
// {
|
||||
// container: docker.Container{
|
||||
// container: docker.ContainerJSON{
|
||||
// Name: "bar",
|
||||
// Config: &docker.Config{},
|
||||
// Config: &container.Config{},
|
||||
// NetworkSettings: &docker.NetworkSettings{
|
||||
// Ports: map[docker.Port][]docker.PortBinding{
|
||||
// "80/tcp": []docker.PortBinding{},
|
||||
@@ -244,16 +251,20 @@ func TestDockerGetPort(t *testing.T) {
|
||||
// expected: "80",
|
||||
// },
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "test",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.port": "8080",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -273,20 +284,24 @@ func TestDockerGetWeight(t *testing.T) {
|
||||
provider := &Docker{}
|
||||
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "foo",
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "0",
|
||||
expected: "1",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "test",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.weight": "10",
|
||||
},
|
||||
@@ -310,20 +325,24 @@ func TestDockerGetDomain(t *testing.T) {
|
||||
}
|
||||
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "foo",
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "docker.localhost",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "test",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.domain": "foo.bar",
|
||||
},
|
||||
@@ -345,20 +364,24 @@ func TestDockerGetProtocol(t *testing.T) {
|
||||
provider := &Docker{}
|
||||
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "foo",
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "http",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "test",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.protocol": "https",
|
||||
},
|
||||
@@ -380,20 +403,24 @@ func TestDockerGetPassHostHeader(t *testing.T) {
|
||||
provider := &Docker{}
|
||||
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "foo",
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "false",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Name: "test",
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.passHostHeader": "true",
|
||||
},
|
||||
@@ -413,18 +440,18 @@ func TestDockerGetPassHostHeader(t *testing.T) {
|
||||
|
||||
func TestDockerGetLabel(t *testing.T) {
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "Label not found:",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
@@ -450,20 +477,20 @@ func TestDockerGetLabel(t *testing.T) {
|
||||
|
||||
func TestDockerGetLabels(t *testing.T) {
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
container docker.ContainerJSON
|
||||
expectedLabels map[string]string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expectedLabels: map[string]string{},
|
||||
expectedError: "Label not found:",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"foo": "fooz",
|
||||
},
|
||||
@@ -475,8 +502,8 @@ func TestDockerGetLabels(t *testing.T) {
|
||||
expectedError: "Label not found: bar",
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"foo": "fooz",
|
||||
"bar": "barz",
|
||||
@@ -506,141 +533,168 @@ func TestDockerGetLabels(t *testing.T) {
|
||||
|
||||
func TestDockerTraefikFilter(t *testing.T) {
|
||||
containers := []struct {
|
||||
container docker.Container
|
||||
container docker.ContainerJSON
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{},
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.enable": "false",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Host",
|
||||
"traefik.frontend.rule": "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.value": "foo.bar",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
"443/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
"443/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.port": "80",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
"443/tcp": {},
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
"443/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.enable": "true",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.enable": "anything",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
container: docker.Container{
|
||||
Config: &docker.Config{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Host",
|
||||
"traefik.frontend.value": "foo.bar",
|
||||
"traefik.frontend.rule": "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -651,33 +705,37 @@ func TestDockerTraefikFilter(t *testing.T) {
|
||||
for _, e := range containers {
|
||||
actual := containerFilter(e.container)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %v, got %v", e.expected, actual)
|
||||
t.Fatalf("expected %v for %+v, got %+v", e.expected, e, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerLoadDockerConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
containers []docker.Container
|
||||
containers []docker.ContainerJSON
|
||||
expectedFrontends map[string]*types.Frontend
|
||||
expectedBackends map[string]*types.Backend
|
||||
}{
|
||||
{
|
||||
containers: []docker.Container{},
|
||||
containers: []docker.ContainerJSON{},
|
||||
expectedFrontends: map[string]*types.Frontend{},
|
||||
expectedBackends: map[string]*types.Backend{},
|
||||
},
|
||||
{
|
||||
containers: []docker.Container{
|
||||
containers: []docker.ContainerJSON{
|
||||
{
|
||||
Name: "test",
|
||||
Config: &docker.Config{},
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
Networks: map[string]docker.ContainerNetwork{
|
||||
"bridgde": {
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"bridge": {
|
||||
IPAddress: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
@@ -685,13 +743,12 @@ func TestDockerLoadDockerConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expectedFrontends: map[string]*types.Frontend{
|
||||
`"frontend-Host-test-docker-localhost"`: {
|
||||
"frontend-Host-test-docker-localhost": {
|
||||
Backend: "backend-test",
|
||||
EntryPoints: []string{},
|
||||
Routes: map[string]types.Route{
|
||||
`"route-frontend-Host-test-docker-localhost"`: {
|
||||
Rule: "Host",
|
||||
Value: "test.docker.localhost",
|
||||
"route-frontend-Host-test-docker-localhost": {
|
||||
Rule: "Host:test.docker.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -700,7 +757,8 @@ func TestDockerLoadDockerConfig(t *testing.T) {
|
||||
"backend-test": {
|
||||
Servers: map[string]types.Server{
|
||||
"server-test": {
|
||||
URL: "http://127.0.0.1:80",
|
||||
URL: "http://127.0.0.1:80",
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
CircuitBreaker: nil,
|
||||
@@ -709,39 +767,47 @@ func TestDockerLoadDockerConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
containers: []docker.Container{
|
||||
containers: []docker.ContainerJSON{
|
||||
{
|
||||
Name: "test1",
|
||||
Config: &docker.Config{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test1",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.backend": "foobar",
|
||||
"traefik.frontend.entryPoints": "http,https",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
Networks: map[string]docker.ContainerNetwork{
|
||||
"bridgde": {
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"bridge": {
|
||||
IPAddress: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "test2",
|
||||
Config: &docker.Config{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test2",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.backend": "foobar",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
Ports: map[docker.Port][]docker.PortBinding{
|
||||
"80/tcp": {},
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
Networks: map[string]docker.ContainerNetwork{
|
||||
"bridgde": {
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"bridge": {
|
||||
IPAddress: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
@@ -749,23 +815,21 @@ func TestDockerLoadDockerConfig(t *testing.T) {
|
||||
},
|
||||
},
|
||||
expectedFrontends: map[string]*types.Frontend{
|
||||
`"frontend-Host-test1-docker-localhost"`: {
|
||||
"frontend-Host-test1-docker-localhost": {
|
||||
Backend: "backend-foobar",
|
||||
EntryPoints: []string{"http", "https"},
|
||||
Routes: map[string]types.Route{
|
||||
`"route-frontend-Host-test1-docker-localhost"`: {
|
||||
Rule: "Host",
|
||||
Value: "test1.docker.localhost",
|
||||
"route-frontend-Host-test1-docker-localhost": {
|
||||
Rule: "Host:test1.docker.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
`"frontend-Host-test2-docker-localhost"`: {
|
||||
"frontend-Host-test2-docker-localhost": {
|
||||
Backend: "backend-foobar",
|
||||
EntryPoints: []string{},
|
||||
Routes: map[string]types.Route{
|
||||
`"route-frontend-Host-test2-docker-localhost"`: {
|
||||
Rule: "Host",
|
||||
Value: "test2.docker.localhost",
|
||||
"route-frontend-Host-test2-docker-localhost": {
|
||||
Rule: "Host:test2.docker.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -774,10 +838,12 @@ func TestDockerLoadDockerConfig(t *testing.T) {
|
||||
"backend-foobar": {
|
||||
Servers: map[string]types.Server{
|
||||
"server-test1": {
|
||||
URL: "http://127.0.0.1:80",
|
||||
URL: "http://127.0.0.1:80",
|
||||
Weight: 1,
|
||||
},
|
||||
"server-test2": {
|
||||
URL: "http://127.0.0.1:80",
|
||||
URL: "http://127.0.0.1:80",
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
CircuitBreaker: nil,
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/etcd"
|
||||
@@ -13,8 +14,8 @@ type Etcd struct {
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Etcd) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
func (provider *Etcd) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
|
||||
provider.storeType = store.ETCD
|
||||
etcd.Register()
|
||||
return provider.provide(configurationChan)
|
||||
return provider.provide(configurationChan, pool)
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"gopkg.in/fsnotify.v1"
|
||||
)
|
||||
@@ -18,7 +19,7 @@ type File struct {
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *File) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
func (provider *File) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Error("Error creating file watcher", err)
|
||||
@@ -34,10 +35,12 @@ func (provider *File) Provide(configurationChan chan<- types.ConfigMessage) erro
|
||||
|
||||
if provider.Watch {
|
||||
// Process events
|
||||
go func() {
|
||||
pool.Go(func(stop chan bool) {
|
||||
defer watcher.Close()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case event := <-watcher.Events:
|
||||
if strings.Contains(event.Name, file.Name()) {
|
||||
log.Debug("File event:", event)
|
||||
@@ -53,7 +56,7 @@ func (provider *File) Provide(configurationChan chan<- types.ConfigMessage) erro
|
||||
log.Error("Watcher event error", error)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
err = watcher.Add(filepath.Dir(file.Name()))
|
||||
if err != nil {
|
||||
log.Error("Error adding file watcher", err)
|
||||
|
274
provider/k8s/client.go
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
100
provider/kv.go
@@ -10,8 +10,11 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"errors"
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv"
|
||||
"github.com/docker/libkv/store"
|
||||
@@ -35,28 +38,42 @@ type KvTLS struct {
|
||||
InsecureSkipVerify bool
|
||||
}
|
||||
|
||||
func (provider *Kv) watchKv(configurationChan chan<- types.ConfigMessage, prefix string) {
|
||||
for {
|
||||
chanKeys, err := provider.kvclient.WatchTree(provider.Prefix, make(chan struct{}) /* stop chan */)
|
||||
func (provider *Kv) watchKv(configurationChan chan<- types.ConfigMessage, prefix string, stop chan bool) error {
|
||||
operation := func() error {
|
||||
events, err := provider.kvclient.WatchTree(provider.Prefix, make(chan struct{}))
|
||||
if err != nil {
|
||||
log.Errorf("Failed to WatchTree %s", err)
|
||||
continue
|
||||
return fmt.Errorf("Failed to KV WatchTree: %v", err)
|
||||
}
|
||||
|
||||
for range chanKeys {
|
||||
configuration := provider.loadConfig()
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: string(provider.storeType),
|
||||
Configuration: configuration,
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return nil
|
||||
case _, ok := <-events:
|
||||
if !ok {
|
||||
return errors.New("watchtree channel closed")
|
||||
}
|
||||
configuration := provider.loadConfig()
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: string(provider.storeType),
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Warnf("Intermittent failure to WatchTree KV. Retrying.")
|
||||
}
|
||||
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("KV connection error: %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot connect to KV server: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
|
||||
storeConfig := &store.Config{
|
||||
ConnectionTimeout: 30 * time.Second,
|
||||
Bucket: "traefik",
|
||||
@@ -88,25 +105,40 @@ func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage) error
|
||||
}
|
||||
}
|
||||
|
||||
kv, err := libkv.NewStore(
|
||||
provider.storeType,
|
||||
strings.Split(provider.Endpoint, ","),
|
||||
storeConfig,
|
||||
)
|
||||
operation := func() error {
|
||||
kv, err := libkv.NewStore(
|
||||
provider.storeType,
|
||||
strings.Split(provider.Endpoint, ","),
|
||||
storeConfig,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to Connect to KV store: %v", err)
|
||||
}
|
||||
if _, err := kv.Exists("qmslkjdfmqlskdjfmqlksjazçueznbvbwzlkajzebvkwjdcqmlsfj"); err != nil {
|
||||
return fmt.Errorf("Failed to test KV store connection: %v", err)
|
||||
}
|
||||
provider.kvclient = kv
|
||||
if provider.Watch {
|
||||
pool.Go(func(stop chan bool) {
|
||||
err := provider.watchKv(configurationChan, provider.Prefix, stop)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot watch KV store: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
configuration := provider.loadConfig()
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: string(provider.storeType),
|
||||
Configuration: configuration,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("KV connection error: %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := kv.List(""); err != nil {
|
||||
return err
|
||||
}
|
||||
provider.kvclient = kv
|
||||
if provider.Watch {
|
||||
go provider.watchKv(configurationChan, provider.Prefix)
|
||||
}
|
||||
configuration := provider.loadConfig()
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: string(provider.storeType),
|
||||
Configuration: configuration,
|
||||
return fmt.Errorf("Cannot connect to KV server: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -141,7 +173,7 @@ func (provider *Kv) list(keys ...string) []string {
|
||||
}
|
||||
directoryKeys := make(map[string]string)
|
||||
for _, key := range keysPairs {
|
||||
directory := strings.Split(strings.TrimPrefix(key.Key, strings.TrimPrefix(joinedKeys, "/")), "/")[0]
|
||||
directory := strings.Split(strings.TrimPrefix(key.Key, joinedKeys), "/")[0]
|
||||
directoryKeys[directory] = joinedKeys + directory
|
||||
}
|
||||
return fun.Values(directoryKeys).([]string)
|
||||
@@ -149,7 +181,7 @@ func (provider *Kv) list(keys ...string) []string {
|
||||
|
||||
func (provider *Kv) get(defaultValue string, keys ...string) string {
|
||||
joinedKeys := strings.Join(keys, "")
|
||||
keyPair, err := provider.kvclient.Get(joinedKeys)
|
||||
keyPair, err := provider.kvclient.Get(strings.TrimPrefix(joinedKeys, "/"))
|
||||
if err != nil {
|
||||
log.Warnf("Error getting key %s %s, setting default %s", joinedKeys, err, defaultValue)
|
||||
return defaultValue
|
||||
|
@@ -80,7 +80,7 @@ func TestKvList(t *testing.T) {
|
||||
},
|
||||
},
|
||||
keys: []string{"foo", "/baz/"},
|
||||
expected: []string{"foo/baz/biz", "foo/baz/1", "foo/baz/2"},
|
||||
expected: []string{"foo/baz/1", "foo/baz/2"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -256,7 +256,9 @@ func TestKvWatchTree(t *testing.T) {
|
||||
}
|
||||
|
||||
configChan := make(chan types.ConfigMessage)
|
||||
go provider.watchKv(configChan, "prefix")
|
||||
go func() {
|
||||
provider.watchKv(configChan, "prefix", make(chan bool, 1))
|
||||
}()
|
||||
|
||||
select {
|
||||
case c1 := <-returnedChans:
|
||||
@@ -336,7 +338,7 @@ func (s *Mock) List(prefix string) ([]*store.KVPair, error) {
|
||||
}
|
||||
kv := []*store.KVPair{}
|
||||
for _, kvPair := range s.KVPairs {
|
||||
if strings.HasPrefix(kvPair.Key, prefix) {
|
||||
if strings.HasPrefix(kvPair.Key, prefix) && !strings.ContainsAny(strings.TrimPrefix(kvPair.Key, prefix), "/") {
|
||||
kv = append(kv, kvPair)
|
||||
}
|
||||
}
|
||||
@@ -362,3 +364,86 @@ func (s *Mock) AtomicDelete(key string, previous *store.KVPair) (bool, error) {
|
||||
func (s *Mock) Close() {
|
||||
return
|
||||
}
|
||||
|
||||
func TestKVLoadConfig(t *testing.T) {
|
||||
provider := &Kv{
|
||||
Prefix: "traefik",
|
||||
kvclient: &Mock{
|
||||
KVPairs: []*store.KVPair{
|
||||
{
|
||||
Key: "traefik/frontends/frontend.with.dot",
|
||||
Value: []byte(""),
|
||||
},
|
||||
{
|
||||
Key: "traefik/frontends/frontend.with.dot/backend",
|
||||
Value: []byte("backend.with.dot.too"),
|
||||
},
|
||||
{
|
||||
Key: "traefik/frontends/frontend.with.dot/routes",
|
||||
Value: []byte(""),
|
||||
},
|
||||
{
|
||||
Key: "traefik/frontends/frontend.with.dot/routes/route.with.dot",
|
||||
Value: []byte(""),
|
||||
},
|
||||
{
|
||||
Key: "traefik/frontends/frontend.with.dot/routes/route.with.dot/rule",
|
||||
Value: []byte("Host:test.localhost"),
|
||||
},
|
||||
{
|
||||
Key: "traefik/backends/backend.with.dot.too",
|
||||
Value: []byte(""),
|
||||
},
|
||||
{
|
||||
Key: "traefik/backends/backend.with.dot.too/servers",
|
||||
Value: []byte(""),
|
||||
},
|
||||
{
|
||||
Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot",
|
||||
Value: []byte(""),
|
||||
},
|
||||
{
|
||||
Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot/url",
|
||||
Value: []byte("http://172.17.0.2:80"),
|
||||
},
|
||||
{
|
||||
Key: "traefik/backends/backend.with.dot.too/servers/server.with.dot/weight",
|
||||
Value: []byte("1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
actual := provider.loadConfig()
|
||||
expected := &types.Configuration{
|
||||
Backends: map[string]*types.Backend{
|
||||
"backend.with.dot.too": {
|
||||
Servers: map[string]types.Server{
|
||||
"server.with.dot": {
|
||||
URL: "http://172.17.0.2:80",
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
CircuitBreaker: nil,
|
||||
LoadBalancer: nil,
|
||||
},
|
||||
},
|
||||
Frontends: map[string]*types.Frontend{
|
||||
"frontend.with.dot": {
|
||||
Backend: "backend.with.dot.too",
|
||||
PassHostHeader: false,
|
||||
EntryPoints: []string{},
|
||||
Routes: map[string]types.Route{
|
||||
"route.with.dot": {
|
||||
Rule: "Host:test.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Backends, expected.Backends) {
|
||||
t.Fatalf("expected %+v, got %+v", expected.Backends, actual.Backends)
|
||||
}
|
||||
if !reflect.DeepEqual(actual.Frontends, expected.Frontends) {
|
||||
t.Fatalf("expected %+v, got %+v", expected.Frontends, actual.Frontends)
|
||||
}
|
||||
}
|
||||
|
@@ -10,9 +10,12 @@ import (
|
||||
"crypto/tls"
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/gambol99/go-marathon"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Marathon holds configuration of the Marathon provider.
|
||||
@@ -39,50 +42,65 @@ type lightMarathonClient interface {
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
config := marathon.NewDefaultConfig()
|
||||
config.URL = provider.Endpoint
|
||||
config.EventsTransport = marathon.EventsTransportSSE
|
||||
if provider.Basic != nil {
|
||||
config.HTTPBasicAuthUser = provider.Basic.HTTPBasicAuthUser
|
||||
config.HTTPBasicPassword = provider.Basic.HTTPBasicPassword
|
||||
}
|
||||
config.HTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: provider.TLS,
|
||||
},
|
||||
}
|
||||
client, err := marathon.NewClient(config)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create a client for marathon, error: %s", err)
|
||||
return err
|
||||
}
|
||||
provider.marathonClient = client
|
||||
update := make(marathon.EventsChannel, 5)
|
||||
if provider.Watch {
|
||||
if err := client.AddEventsListener(update, marathon.EVENTS_APPLICATIONS); err != nil {
|
||||
log.Errorf("Failed to register for events, %s", err)
|
||||
} else {
|
||||
go func() {
|
||||
func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
|
||||
operation := func() error {
|
||||
config := marathon.NewDefaultConfig()
|
||||
config.URL = provider.Endpoint
|
||||
config.EventsTransport = marathon.EventsTransportSSE
|
||||
if provider.Basic != nil {
|
||||
config.HTTPBasicAuthUser = provider.Basic.HTTPBasicAuthUser
|
||||
config.HTTPBasicPassword = provider.Basic.HTTPBasicPassword
|
||||
}
|
||||
config.HTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: provider.TLS,
|
||||
},
|
||||
}
|
||||
client, err := marathon.NewClient(config)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create a client for marathon, error: %s", err)
|
||||
return err
|
||||
}
|
||||
provider.marathonClient = client
|
||||
update := make(marathon.EventsChannel, 5)
|
||||
if provider.Watch {
|
||||
if err := client.AddEventsListener(update, marathon.EVENTS_APPLICATIONS); err != nil {
|
||||
log.Errorf("Failed to register for events, %s", err)
|
||||
return err
|
||||
}
|
||||
pool.Go(func(stop chan bool) {
|
||||
defer close(update)
|
||||
for {
|
||||
event := <-update
|
||||
log.Debug("Marathon event receveived", event)
|
||||
configuration := provider.loadMarathonConfig()
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "marathon",
|
||||
Configuration: configuration,
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case event := <-update:
|
||||
log.Debug("Marathon event receveived", event)
|
||||
configuration := provider.loadMarathonConfig()
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "marathon",
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
configuration := provider.loadMarathonConfig()
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "marathon",
|
||||
Configuration: configuration,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
configuration := provider.loadMarathonConfig()
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "marathon",
|
||||
Configuration: configuration,
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Marathon connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot connect to Marathon server %+v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -96,7 +114,6 @@ func (provider *Marathon) loadMarathonConfig() *types.Configuration {
|
||||
"getProtocol": provider.getProtocol,
|
||||
"getPassHostHeader": provider.getPassHostHeader,
|
||||
"getEntryPoints": provider.getEntryPoints,
|
||||
"getFrontendValue": provider.getFrontendValue,
|
||||
"getFrontendRule": provider.getFrontendRule,
|
||||
"getFrontendBackend": provider.getFrontendBackend,
|
||||
"replace": replace,
|
||||
@@ -309,22 +326,21 @@ func (provider *Marathon) getEntryPoints(application marathon.Application) []str
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// getFrontendValue returns the frontend value for the specified application, using
|
||||
// it's label. It returns a default one if the label is not present.
|
||||
func (provider *Marathon) getFrontendValue(application marathon.Application) string {
|
||||
if label, err := provider.getLabel(application, "traefik.frontend.value"); err == nil {
|
||||
return label
|
||||
}
|
||||
return getEscapedName(application.ID) + "." + provider.Domain
|
||||
}
|
||||
|
||||
// getFrontendRule returns the frontend rule for the specified application, using
|
||||
// it's label. It returns a default one (Host) if the label is not present.
|
||||
func (provider *Marathon) getFrontendRule(application marathon.Application) string {
|
||||
// ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
|
||||
// TODO: backwards compatibility with DEPRECATED rule.Value
|
||||
if value, err := provider.getLabel(application, "traefik.frontend.value"); err == nil {
|
||||
log.Warnf("Label traefik.frontend.value=%s is DEPRECATED, please refer to the rule label: https://github.com/containous/traefik/blob/master/docs/index.md#marathon", value)
|
||||
rule, _ := provider.getLabel(application, "traefik.frontend.rule")
|
||||
return rule + ":" + value
|
||||
}
|
||||
// ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
|
||||
if label, err := provider.getLabel(application, "traefik.frontend.rule"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "Host"
|
||||
return "Host:" + getEscapedName(application.ID) + "." + provider.Domain
|
||||
}
|
||||
|
||||
func (provider *Marathon) getBackend(task marathon.Task, applications []marathon.Application) string {
|
||||
|
@@ -86,8 +86,7 @@ func TestMarathonLoadConfig(t *testing.T) {
|
||||
EntryPoints: []string{},
|
||||
Routes: map[string]types.Route{
|
||||
`route-host-test`: {
|
||||
Rule: "Host",
|
||||
Value: "test.docker.localhost",
|
||||
Rule: "Host:test.docker.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -831,7 +830,7 @@ func TestMarathonGetEntryPoints(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonGetFrontendValue(t *testing.T) {
|
||||
func TestMarathonGetFrontendRule(t *testing.T) {
|
||||
provider := &Marathon{
|
||||
Domain: "docker.localhost",
|
||||
}
|
||||
@@ -842,50 +841,21 @@ func TestMarathonGetFrontendValue(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
application: marathon.Application{},
|
||||
expected: ".docker.localhost",
|
||||
expected: "Host:.docker.localhost",
|
||||
},
|
||||
{
|
||||
application: marathon.Application{
|
||||
ID: "test",
|
||||
},
|
||||
expected: "test.docker.localhost",
|
||||
expected: "Host:test.docker.localhost",
|
||||
},
|
||||
{
|
||||
application: marathon.Application{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.value": "foo.bar",
|
||||
"traefik.frontend.rule": "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
expected: "foo.bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, a := range applications {
|
||||
actual := provider.getFrontendValue(a.application)
|
||||
if actual != a.expected {
|
||||
t.Fatalf("expected %q, got %q", a.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonGetFrontendRule(t *testing.T) {
|
||||
provider := &Marathon{}
|
||||
|
||||
applications := []struct {
|
||||
application marathon.Application
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
application: marathon.Application{},
|
||||
expected: "Host",
|
||||
},
|
||||
{
|
||||
application: marathon.Application{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Header",
|
||||
},
|
||||
},
|
||||
expected: "Header",
|
||||
expected: "Host:foo.bar",
|
||||
},
|
||||
}
|
||||
|
||||
|
@@ -8,14 +8,16 @@ import (
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/containous/traefik/autogen"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Provider defines methods of a provider.
|
||||
type Provider interface {
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
Provide(configurationChan chan<- types.ConfigMessage) error
|
||||
Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error
|
||||
}
|
||||
|
||||
// BaseProvider should be inherited by providers
|
||||
@@ -67,3 +69,11 @@ func replace(s1 string, s2 string, s3 string) string {
|
||||
func getEscapedName(name string) string {
|
||||
return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1)
|
||||
}
|
||||
|
||||
func normalize(name string) string {
|
||||
fargs := func(c rune) bool {
|
||||
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
|
||||
}
|
||||
// get function
|
||||
return strings.Join(strings.FieldsFunc(name, fargs), "-")
|
||||
}
|
||||
|
@@ -74,7 +74,7 @@ func TestConfigurationErrors(t *testing.T) {
|
||||
Filename: templateInvalidTOMLFile.Name(),
|
||||
},
|
||||
},
|
||||
expectedError: "Near line 1, key 'Hello': Near line 1: Expected key separator '=', but got '<' instead",
|
||||
expectedError: "Near line 1 (last key parsed 'Hello'): Expected key separator '=', but got '<' instead",
|
||||
funcMap: template.FuncMap{
|
||||
"Foo": func() string {
|
||||
return "bar"
|
||||
@@ -168,3 +168,41 @@ func TestReplace(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigurationReturnsCorrectMaxConnConfiguration(t *testing.T) {
|
||||
templateFile, err := ioutil.TempFile("", "provider-configuration")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(templateFile.Name())
|
||||
data := []byte(`[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.maxconn]
|
||||
amount = 10
|
||||
extractorFunc = "request.host"`)
|
||||
err = ioutil.WriteFile(templateFile.Name(), data, 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
provider := &myProvider{
|
||||
BaseProvider{
|
||||
Filename: templateFile.Name(),
|
||||
},
|
||||
}
|
||||
configuration, err := provider.getConfiguration(templateFile.Name(), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Shouldn't have error out, got %v", err)
|
||||
}
|
||||
if configuration == nil {
|
||||
t.Fatalf("Configuration should not be nil, but was")
|
||||
}
|
||||
|
||||
if configuration.Backends["backend1"].MaxConn.Amount != 10 {
|
||||
t.Fatalf("Configuration did not parse MaxConn.Amount properly")
|
||||
}
|
||||
|
||||
if configuration.Backends["backend1"].MaxConn.ExtractorFunc != "request.host" {
|
||||
t.Fatalf("Configuration did not parse MaxConn.ExtractorFunc properly")
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/zookeeper"
|
||||
@@ -13,8 +14,8 @@ type Zookepper struct {
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Zookepper) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
func (provider *Zookepper) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
|
||||
provider.storeType = store.ZK
|
||||
zookeeper.Register()
|
||||
return provider.provide(configurationChan)
|
||||
return provider.provide(configurationChan, pool)
|
||||
}
|
||||
|
147
rules.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gorilla/mux"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Rules holds rule parsing and configuration
|
||||
type Rules struct {
|
||||
route *serverRoute
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *Rules) host(hosts ...string) *mux.Route {
|
||||
return r.route.route.MatcherFunc(func(req *http.Request, route *mux.RouteMatch) bool {
|
||||
reqHost, _, err := net.SplitHostPort(req.Host)
|
||||
if err != nil {
|
||||
reqHost = req.Host
|
||||
}
|
||||
for _, host := range hosts {
|
||||
if reqHost == strings.TrimSpace(host) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Rules) hostRegexp(hosts ...string) *mux.Route {
|
||||
router := r.route.route.Subrouter()
|
||||
for _, host := range hosts {
|
||||
router.Host(strings.TrimSpace(host))
|
||||
}
|
||||
return r.route.route
|
||||
}
|
||||
|
||||
func (r *Rules) path(paths ...string) *mux.Route {
|
||||
router := r.route.route.Subrouter()
|
||||
for _, path := range paths {
|
||||
router.Path(strings.TrimSpace(path))
|
||||
}
|
||||
return r.route.route
|
||||
}
|
||||
|
||||
func (r *Rules) pathPrefix(paths ...string) *mux.Route {
|
||||
router := r.route.route.Subrouter()
|
||||
for _, path := range paths {
|
||||
router.PathPrefix(strings.TrimSpace(path))
|
||||
}
|
||||
return r.route.route
|
||||
}
|
||||
|
||||
type bySize []string
|
||||
|
||||
func (a bySize) Len() int { return len(a) }
|
||||
func (a bySize) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a bySize) Less(i, j int) bool { return len(a[i]) > len(a[j]) }
|
||||
|
||||
func (r *Rules) pathStrip(paths ...string) *mux.Route {
|
||||
sort.Sort(bySize(paths))
|
||||
r.route.stripPrefixes = paths
|
||||
router := r.route.route.Subrouter()
|
||||
for _, path := range paths {
|
||||
router.Path(strings.TrimSpace(path))
|
||||
}
|
||||
return r.route.route
|
||||
}
|
||||
|
||||
func (r *Rules) pathPrefixStrip(paths ...string) *mux.Route {
|
||||
sort.Sort(bySize(paths))
|
||||
r.route.stripPrefixes = paths
|
||||
router := r.route.route.Subrouter()
|
||||
for _, path := range paths {
|
||||
router.PathPrefix(strings.TrimSpace(path))
|
||||
}
|
||||
return r.route.route
|
||||
}
|
||||
|
||||
func (r *Rules) methods(methods ...string) *mux.Route {
|
||||
return r.route.route.Methods(methods...)
|
||||
}
|
||||
|
||||
func (r *Rules) headers(headers ...string) *mux.Route {
|
||||
return r.route.route.Headers(headers...)
|
||||
}
|
||||
|
||||
func (r *Rules) headersRegexp(headers ...string) *mux.Route {
|
||||
return r.route.route.HeadersRegexp(headers...)
|
||||
}
|
||||
|
||||
// Parse parses rules expressions
|
||||
func (r *Rules) Parse(expression string) (*mux.Route, error) {
|
||||
functions := map[string]interface{}{
|
||||
"Host": r.host,
|
||||
"HostRegexp": r.hostRegexp,
|
||||
"Path": r.path,
|
||||
"PathStrip": r.pathStrip,
|
||||
"PathPrefix": r.pathPrefix,
|
||||
"PathPrefixStrip": r.pathPrefixStrip,
|
||||
"Method": r.methods,
|
||||
"Headers": r.headers,
|
||||
"HeadersRegexp": r.headersRegexp,
|
||||
}
|
||||
f := func(c rune) bool {
|
||||
return c == ':'
|
||||
}
|
||||
// get function
|
||||
parsedFunctions := strings.FieldsFunc(expression, f)
|
||||
if len(parsedFunctions) == 0 {
|
||||
return nil, errors.New("Error parsing rule: " + expression)
|
||||
}
|
||||
parsedFunction, ok := functions[parsedFunctions[0]]
|
||||
if !ok {
|
||||
return nil, errors.New("Error parsing rule: " + expression + ". Unknown function: " + parsedFunctions[0])
|
||||
}
|
||||
parsedFunctions = append(parsedFunctions[:0], parsedFunctions[1:]...)
|
||||
fargs := func(c rune) bool {
|
||||
return c == ',' || c == ';'
|
||||
}
|
||||
// get function
|
||||
parsedArgs := strings.FieldsFunc(strings.Join(parsedFunctions, ":"), fargs)
|
||||
if len(parsedArgs) == 0 {
|
||||
return nil, errors.New("Error parsing args from rule: " + expression)
|
||||
}
|
||||
|
||||
inputs := make([]reflect.Value, len(parsedArgs))
|
||||
for i := range parsedArgs {
|
||||
inputs[i] = reflect.ValueOf(parsedArgs[i])
|
||||
}
|
||||
method := reflect.ValueOf(parsedFunction)
|
||||
if method.IsValid() {
|
||||
resultRoute := method.Call(inputs)[0].Interface().(*mux.Route)
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if resultRoute.GetError() != nil {
|
||||
return nil, resultRoute.GetError()
|
||||
}
|
||||
return resultRoute, nil
|
||||
}
|
||||
return nil, errors.New("Method not found: " + parsedFunctions[0])
|
||||
}
|
70
safe/routine.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package safe
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type routine struct {
|
||||
goroutine func(chan bool)
|
||||
stop chan bool
|
||||
}
|
||||
|
||||
// Pool creates a pool of go routines
|
||||
type Pool struct {
|
||||
routines []routine
|
||||
waitGroup sync.WaitGroup
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// Go starts a recoverable goroutine, and can be stopped with stop chan
|
||||
func (p *Pool) Go(goroutine func(stop chan bool)) {
|
||||
p.lock.Lock()
|
||||
newRoutine := routine{
|
||||
goroutine: goroutine,
|
||||
stop: make(chan bool, 1),
|
||||
}
|
||||
p.routines = append(p.routines, newRoutine)
|
||||
p.waitGroup.Add(1)
|
||||
Go(func() {
|
||||
goroutine(newRoutine.stop)
|
||||
p.waitGroup.Done()
|
||||
})
|
||||
p.lock.Unlock()
|
||||
}
|
||||
|
||||
// Stop stops all started routines, waiting for their termination
|
||||
func (p *Pool) Stop() {
|
||||
p.lock.Lock()
|
||||
for _, routine := range p.routines {
|
||||
routine.stop <- true
|
||||
}
|
||||
p.waitGroup.Wait()
|
||||
for _, routine := range p.routines {
|
||||
close(routine.stop)
|
||||
}
|
||||
p.lock.Unlock()
|
||||
}
|
||||
|
||||
// Go starts a recoverable goroutine
|
||||
func Go(goroutine func()) {
|
||||
GoWithRecover(goroutine, defaultRecoverGoroutine)
|
||||
}
|
||||
|
||||
// GoWithRecover starts a recoverable goroutine using given customRecover() function
|
||||
func GoWithRecover(goroutine func(), customRecover func(err interface{})) {
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
customRecover(err)
|
||||
}
|
||||
}()
|
||||
goroutine()
|
||||
}()
|
||||
}
|
||||
|
||||
func defaultRecoverGoroutine(err interface{}) {
|
||||
log.Println(err)
|
||||
debug.PrintStack()
|
||||
}
|
30
safe/safe.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package safe
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Safe contains a thread-safe value
|
||||
type Safe struct {
|
||||
value interface{}
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// New create a new Safe instance given a value
|
||||
func New(value interface{}) *Safe {
|
||||
return &Safe{value: value, lock: sync.RWMutex{}}
|
||||
}
|
||||
|
||||
// Get returns the value
|
||||
func (s *Safe) Get() interface{} {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
return s.value
|
||||
}
|
||||
|
||||
// Set sets a new value
|
||||
func (s *Safe) Set(value interface{}) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.value = value
|
||||
}
|
@@ -22,4 +22,4 @@ if [ -z "$DATE" ]; then
|
||||
fi
|
||||
|
||||
# Build binaries
|
||||
CGO_ENABLED=0 GOGC=off go build $FLAGS -ldflags "-X main.Version=$VERSION -X main.BuildDate=$DATE" -a -installsuffix nocgo -o dist/traefik .
|
||||
CGO_ENABLED=0 GOGC=off go build $FLAGS -ldflags "-s -w -X main.Version=$VERSION -X main.BuildDate=$DATE" -a -installsuffix nocgo -o dist/traefik .
|
||||
|
@@ -2,34 +2,64 @@
|
||||
set -e
|
||||
|
||||
if ([ "$TRAVIS_BRANCH" = "master" ] || [ ! -z "$TRAVIS_TAG" ]) && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||
echo "Deploying"
|
||||
echo "Deploying..."
|
||||
else
|
||||
echo "Skipping deploy"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
curl -LO https://github.com/tcnksm/ghr/releases/download/pre-release/linux_amd64.zip
|
||||
git config --global user.email "emile@vauge.com"
|
||||
git config --global user.name "Emile Vauge"
|
||||
|
||||
# load ssh key
|
||||
echo "Loading key..."
|
||||
openssl aes-256-cbc -d -k "$pass" -in .travis/traefik.id_rsa.enc -out ~/.ssh/traefik.id_rsa
|
||||
eval "$(ssh-agent -s)"
|
||||
chmod 600 ~/.ssh/traefik.id_rsa
|
||||
ssh-add ~/.ssh/traefik.id_rsa
|
||||
|
||||
# download github release
|
||||
echo "Downloading ghr..."
|
||||
curl -LOs https://github.com/tcnksm/ghr/releases/download/pre-release/linux_amd64.zip
|
||||
unzip -q linux_amd64.zip
|
||||
sudo mv ghr /usr/bin/ghr
|
||||
sudo chmod +x /usr/bin/ghr
|
||||
|
||||
# github release and tag
|
||||
echo "Github release..."
|
||||
ghr -t $GITHUB_TOKEN -u containous -r traefik --prerelease ${VERSION} dist/
|
||||
|
||||
# update docs.traefik.io
|
||||
echo "Generating and updating documentation..."
|
||||
# DOESN'T WORK :'(
|
||||
# git remote add ssh git@github.com:containous/traefik.git
|
||||
# mkdocs gh-deploy -m $VERSION -c -r ssh
|
||||
|
||||
mkdir site
|
||||
cd site
|
||||
git init
|
||||
git remote add origin git@github.com:containous/traefik.git
|
||||
git fetch origin
|
||||
git checkout gh-pages
|
||||
cd ..
|
||||
mkdocs build --clean
|
||||
cd site
|
||||
git add .
|
||||
echo $VERSION | git commit --file -
|
||||
git push -q -f origin gh-pages > /dev/null 2>&1
|
||||
|
||||
# update traefik-library-image repo (official Docker image)
|
||||
git config --global user.email "emile@vauge.com"
|
||||
git config --global user.name "Emile Vauge"
|
||||
git clone https://github.com/containous/traefik-library-image.git
|
||||
echo "Updating traefik-library-imag repo..."
|
||||
git clone git@github.com:containous/traefik-library-image.git
|
||||
cd traefik-library-image
|
||||
git remote rm origin
|
||||
git remote add origin https://emilevauge:${GITHUB_TOKEN}@github.com/containous/traefik-library-image.git
|
||||
./update.sh $VERSION
|
||||
git add -A
|
||||
echo $VERSION | git commit --file -
|
||||
echo $VERSION | git tag -a $VERSION --file -
|
||||
git push --follow-tags -u origin master
|
||||
git push -q --follow-tags -u origin master > /dev/null 2>&1
|
||||
|
||||
# create docker image emilevauge/traefik (compatibility)
|
||||
echo "Updating docker emilevauge/traefik image..."
|
||||
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||
docker tag containous/traefik emilevauge/traefik:latest
|
||||
docker push emilevauge/traefik:latest
|
||||
|
243
server.go
@@ -15,21 +15,24 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/codegangsta/negroni"
|
||||
"github.com/containous/oxy/cbreaker"
|
||||
"github.com/containous/oxy/connlimit"
|
||||
"github.com/containous/oxy/forward"
|
||||
"github.com/containous/oxy/roundrobin"
|
||||
"github.com/containous/oxy/stream"
|
||||
"github.com/containous/oxy/utils"
|
||||
"github.com/containous/traefik/middlewares"
|
||||
"github.com/containous/traefik/provider"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mailgun/manners"
|
||||
"github.com/streamrail/concurrent-map"
|
||||
)
|
||||
|
||||
var oxyLogger = &OxyLogger{}
|
||||
@@ -42,10 +45,10 @@ type Server struct {
|
||||
signals chan os.Signal
|
||||
stopChan chan bool
|
||||
providers []provider.Provider
|
||||
serverLock sync.Mutex
|
||||
currentConfigurations configs
|
||||
currentConfigurations safe.Safe
|
||||
globalConfiguration GlobalConfiguration
|
||||
loggerMiddleware *middlewares.Logger
|
||||
routinesPool safe.Pool
|
||||
}
|
||||
|
||||
type serverEntryPoints map[string]*serverEntryPoint
|
||||
@@ -55,6 +58,11 @@ type serverEntryPoint struct {
|
||||
httpRouter *middlewares.HandlerSwitcher
|
||||
}
|
||||
|
||||
type serverRoute struct {
|
||||
route *mux.Route
|
||||
stripPrefixes []string
|
||||
}
|
||||
|
||||
// NewServer returns an initialized Server.
|
||||
func NewServer(globalConfiguration GlobalConfiguration) *Server {
|
||||
server := new(Server)
|
||||
@@ -63,10 +71,11 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server {
|
||||
server.configurationChan = make(chan types.ConfigMessage, 10)
|
||||
server.configurationValidatedChan = make(chan types.ConfigMessage, 10)
|
||||
server.signals = make(chan os.Signal, 1)
|
||||
server.stopChan = make(chan bool)
|
||||
server.stopChan = make(chan bool, 1)
|
||||
server.providers = []provider.Provider{}
|
||||
signal.Notify(server.signals, syscall.SIGINT, syscall.SIGTERM)
|
||||
server.currentConfigurations = make(configs)
|
||||
currentConfigurations := make(configs)
|
||||
server.currentConfigurations.Set(currentConfigurations)
|
||||
server.globalConfiguration = globalConfiguration
|
||||
server.loggerMiddleware = middlewares.NewLogger(globalConfiguration.AccessLogsFile)
|
||||
|
||||
@@ -76,8 +85,12 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server {
|
||||
// Start starts the server and blocks until server is shutted down.
|
||||
func (server *Server) Start() {
|
||||
server.startHTTPServers()
|
||||
go server.listenProviders()
|
||||
go server.listenConfigurations()
|
||||
server.routinesPool.Go(func(stop chan bool) {
|
||||
server.listenProviders(stop)
|
||||
})
|
||||
server.routinesPool.Go(func(stop chan bool) {
|
||||
server.listenConfigurations(stop)
|
||||
})
|
||||
server.configureProviders()
|
||||
server.startProviders()
|
||||
go server.listenSignals()
|
||||
@@ -94,6 +107,7 @@ func (server *Server) Stop() {
|
||||
|
||||
// Close destroys the server
|
||||
func (server *Server) Close() {
|
||||
server.routinesPool.Stop()
|
||||
close(server.configurationChan)
|
||||
close(server.configurationValidatedChan)
|
||||
close(server.signals)
|
||||
@@ -114,58 +128,79 @@ func (server *Server) startHTTPServers() {
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) listenProviders() {
|
||||
lastReceivedConfiguration := time.Unix(0, 0)
|
||||
lastConfigs := make(map[string]*types.ConfigMessage)
|
||||
func (server *Server) listenProviders(stop chan bool) {
|
||||
lastReceivedConfiguration := safe.New(time.Unix(0, 0))
|
||||
lastConfigs := cmap.New()
|
||||
for {
|
||||
configMsg := <-server.configurationChan
|
||||
jsonConf, _ := json.Marshal(configMsg.Configuration)
|
||||
log.Debugf("Configuration received from provider %s: %s", configMsg.ProviderName, string(jsonConf))
|
||||
lastConfigs[configMsg.ProviderName] = &configMsg
|
||||
if time.Now().After(lastReceivedConfiguration.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) {
|
||||
log.Debugf("Last %s config received more than %s, OK", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration)
|
||||
// last config received more than n s ago
|
||||
server.configurationValidatedChan <- configMsg
|
||||
} else {
|
||||
log.Debugf("Last %s config received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration)
|
||||
go func() {
|
||||
<-time.After(server.globalConfiguration.ProvidersThrottleDuration)
|
||||
if time.Now().After(lastReceivedConfiguration.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) {
|
||||
log.Debugf("Waited for %s config, OK", configMsg.ProviderName)
|
||||
server.configurationValidatedChan <- *lastConfigs[configMsg.ProviderName]
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case configMsg, ok := <-server.configurationChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
jsonConf, _ := json.Marshal(configMsg.Configuration)
|
||||
log.Debugf("Configuration received from provider %s: %s", configMsg.ProviderName, string(jsonConf))
|
||||
lastConfigs.Set(configMsg.ProviderName, &configMsg)
|
||||
lastReceivedConfigurationValue := lastReceivedConfiguration.Get().(time.Time)
|
||||
if time.Now().After(lastReceivedConfigurationValue.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) {
|
||||
log.Debugf("Last %s config received more than %s, OK", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration)
|
||||
// last config received more than n s ago
|
||||
server.configurationValidatedChan <- configMsg
|
||||
} else {
|
||||
log.Debugf("Last %s config received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration)
|
||||
server.routinesPool.Go(func(stop chan bool) {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-time.After(server.globalConfiguration.ProvidersThrottleDuration):
|
||||
lastReceivedConfigurationValue := lastReceivedConfiguration.Get().(time.Time)
|
||||
if time.Now().After(lastReceivedConfigurationValue.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) {
|
||||
log.Debugf("Waited for %s config, OK", configMsg.ProviderName)
|
||||
if lastConfig, ok := lastConfigs.Get(configMsg.ProviderName); ok {
|
||||
server.configurationValidatedChan <- *lastConfig.(*types.ConfigMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
lastReceivedConfiguration.Set(time.Now())
|
||||
}
|
||||
lastReceivedConfiguration = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) listenConfigurations() {
|
||||
func (server *Server) listenConfigurations(stop chan bool) {
|
||||
for {
|
||||
configMsg := <-server.configurationValidatedChan
|
||||
if configMsg.Configuration == nil {
|
||||
log.Info("Skipping empty Configuration")
|
||||
} else if reflect.DeepEqual(server.currentConfigurations[configMsg.ProviderName], configMsg.Configuration) {
|
||||
log.Info("Skipping same configuration")
|
||||
} else {
|
||||
// Copy configurations to new map so we don't change current if LoadConfig fails
|
||||
newConfigurations := make(configs)
|
||||
for k, v := range server.currentConfigurations {
|
||||
newConfigurations[k] = v
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case configMsg, ok := <-server.configurationValidatedChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
newConfigurations[configMsg.ProviderName] = configMsg.Configuration
|
||||
|
||||
newServerEntryPoints, err := server.loadConfig(newConfigurations, server.globalConfiguration)
|
||||
if err == nil {
|
||||
server.serverLock.Lock()
|
||||
for newServerEntryPointName, newServerEntryPoint := range newServerEntryPoints {
|
||||
server.serverEntryPoints[newServerEntryPointName].httpRouter.UpdateHandler(newServerEntryPoint.httpRouter.GetHandler())
|
||||
log.Infof("Server configuration reloaded on %s", server.serverEntryPoints[newServerEntryPointName].httpServer.Addr)
|
||||
}
|
||||
server.currentConfigurations = newConfigurations
|
||||
server.serverLock.Unlock()
|
||||
currentConfigurations := server.currentConfigurations.Get().(configs)
|
||||
if configMsg.Configuration == nil {
|
||||
log.Infof("Skipping empty Configuration for provider %s", configMsg.ProviderName)
|
||||
} else if reflect.DeepEqual(currentConfigurations[configMsg.ProviderName], configMsg.Configuration) {
|
||||
log.Infof("Skipping same configuration for provider %s", configMsg.ProviderName)
|
||||
} else {
|
||||
log.Error("Error loading new configuration, aborted ", err)
|
||||
// Copy configurations to new map so we don't change current if LoadConfig fails
|
||||
newConfigurations := make(configs)
|
||||
for k, v := range currentConfigurations {
|
||||
newConfigurations[k] = v
|
||||
}
|
||||
newConfigurations[configMsg.ProviderName] = configMsg.Configuration
|
||||
|
||||
newServerEntryPoints, err := server.loadConfig(newConfigurations, server.globalConfiguration)
|
||||
if err == nil {
|
||||
for newServerEntryPointName, newServerEntryPoint := range newServerEntryPoints {
|
||||
server.serverEntryPoints[newServerEntryPointName].httpRouter.UpdateHandler(newServerEntryPoint.httpRouter.GetHandler())
|
||||
log.Infof("Server configuration reloaded on %s", server.serverEntryPoints[newServerEntryPointName].httpServer.Addr)
|
||||
}
|
||||
server.currentConfigurations.Set(newConfigurations)
|
||||
} else {
|
||||
log.Error("Error loading new configuration, aborted ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,6 +236,9 @@ func (server *Server) configureProviders() {
|
||||
if server.globalConfiguration.Boltdb != nil {
|
||||
server.providers = append(server.providers, server.globalConfiguration.Boltdb)
|
||||
}
|
||||
if server.globalConfiguration.Kubernetes != nil {
|
||||
server.providers = append(server.providers, server.globalConfiguration.Kubernetes)
|
||||
}
|
||||
}
|
||||
|
||||
func (server *Server) startProviders() {
|
||||
@@ -209,12 +247,12 @@ func (server *Server) startProviders() {
|
||||
jsonConf, _ := json.Marshal(provider)
|
||||
log.Infof("Starting provider %v %s", reflect.TypeOf(provider), jsonConf)
|
||||
currentProvider := provider
|
||||
go func() {
|
||||
err := currentProvider.Provide(server.configurationChan)
|
||||
safe.Go(func() {
|
||||
err := currentProvider.Provide(server.configurationChan, &server.routinesPool)
|
||||
if err != nil {
|
||||
log.Errorf("Error starting provider %s", err)
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +372,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
||||
redirectHandlers := make(map[string]http.Handler)
|
||||
|
||||
backends := map[string]http.Handler{}
|
||||
backend2FrontendMap := map[string]string{}
|
||||
for _, configuration := range configurations {
|
||||
frontendNames := sortedFrontendNamesForConfig(configuration)
|
||||
for _, frontendName := range frontendNames {
|
||||
@@ -341,6 +380,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
||||
|
||||
log.Debugf("Creating frontend %s", frontendName)
|
||||
fwd, _ := forward.New(forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader))
|
||||
saveBackend := middlewares.NewSaveBackend(fwd)
|
||||
// default endpoints if not defined in frontends
|
||||
if len(frontend.EntryPoints) == 0 {
|
||||
frontend.EntryPoints = globalConfiguration.DefaultEntryPoints
|
||||
@@ -354,30 +394,29 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
||||
if _, ok := serverEntryPoints[entryPointName]; !ok {
|
||||
return nil, errors.New("Undefined entrypoint: " + entryPointName)
|
||||
}
|
||||
newRoute := serverEntryPoints[entryPointName].httpRouter.GetHandler().NewRoute().Name(frontendName)
|
||||
newServerRoute := &serverRoute{route: serverEntryPoints[entryPointName].httpRouter.GetHandler().NewRoute().Name(frontendName)}
|
||||
for routeName, route := range frontend.Routes {
|
||||
log.Debugf("Creating route %s %s:%s", routeName, route.Rule, route.Value)
|
||||
route, err := getRoute(newRoute, route.Rule, route.Value)
|
||||
err := getRoute(newServerRoute, &route)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newRoute = route
|
||||
log.Debugf("Creating route %s %s", routeName, route.Rule)
|
||||
}
|
||||
entryPoint := globalConfiguration.EntryPoints[entryPointName]
|
||||
if entryPoint.Redirect != nil {
|
||||
if redirectHandlers[entryPointName] != nil {
|
||||
newRoute.Handler(redirectHandlers[entryPointName])
|
||||
newServerRoute.route.Handler(redirectHandlers[entryPointName])
|
||||
} else if handler, err := server.loadEntryPointConfig(entryPointName, entryPoint); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
newRoute.Handler(handler)
|
||||
newServerRoute.route.Handler(handler)
|
||||
redirectHandlers[entryPointName] = handler
|
||||
}
|
||||
} else {
|
||||
if backends[frontend.Backend] == nil {
|
||||
log.Debugf("Creating backend %s", frontend.Backend)
|
||||
var lb http.Handler
|
||||
rr, _ := roundrobin.New(fwd)
|
||||
rr, _ := roundrobin.New(saveBackend)
|
||||
if configuration.Backends[frontend.Backend] == nil {
|
||||
return nil, errors.New("Undefined backend: " + frontend.Backend)
|
||||
}
|
||||
@@ -395,6 +434,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backend2FrontendMap[url.String()] = frontendName
|
||||
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 {
|
||||
return nil, err
|
||||
@@ -408,15 +448,28 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backend2FrontendMap[url.String()] = frontendName
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
maxConns := configuration.Backends[frontend.Backend].MaxConn
|
||||
if maxConns != nil && maxConns.Amount != 0 {
|
||||
extractFunc, err := utils.NewExtractor(maxConns.ExtractorFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("Creating loadd-balancer connlimit")
|
||||
lb, err = connlimit.New(lb, extractFunc, maxConns.Amount, connlimit.Logger(oxyLogger))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// retry ?
|
||||
if globalConfiguration.Retry != nil {
|
||||
retries := len(configuration.Backends[frontend.Backend].Servers) - 1
|
||||
retries := len(configuration.Backends[frontend.Backend].Servers)
|
||||
if globalConfiguration.Retry.Attempts > 0 {
|
||||
retries = globalConfiguration.Retry.Attempts
|
||||
}
|
||||
@@ -448,41 +501,28 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
||||
} else {
|
||||
log.Debugf("Reusing backend %s", frontend.Backend)
|
||||
}
|
||||
server.wireFrontendBackend(frontend.Routes, newRoute, backends[frontend.Backend])
|
||||
server.wireFrontendBackend(newServerRoute, backends[frontend.Backend])
|
||||
}
|
||||
err := newRoute.GetError()
|
||||
err := newServerRoute.route.GetError()
|
||||
if err != nil {
|
||||
log.Errorf("Error building route: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
middlewares.SetBackend2FrontendMap(&backend2FrontendMap)
|
||||
return serverEntryPoints, nil
|
||||
}
|
||||
|
||||
func (server *Server) wireFrontendBackend(routes map[string]types.Route, newRoute *mux.Route, handler http.Handler) {
|
||||
func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http.Handler) {
|
||||
// strip prefix
|
||||
var strip bool
|
||||
for _, route := range routes {
|
||||
switch route.Rule {
|
||||
case "PathStrip":
|
||||
newRoute.Handler(&middlewares.StripPrefix{
|
||||
Prefix: route.Value,
|
||||
Handler: handler,
|
||||
})
|
||||
strip = true
|
||||
break
|
||||
case "PathPrefixStrip":
|
||||
newRoute.Handler(&middlewares.StripPrefix{
|
||||
Prefix: route.Value,
|
||||
Handler: handler,
|
||||
})
|
||||
strip = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !strip {
|
||||
newRoute.Handler(handler)
|
||||
if len(serverRoute.stripPrefixes) > 0 {
|
||||
serverRoute.route.Handler(&middlewares.StripPrefix{
|
||||
Prefixes: serverRoute.stripPrefixes,
|
||||
Handler: handler,
|
||||
})
|
||||
} else {
|
||||
serverRoute.route.Handler(handler)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,32 +562,29 @@ func (server *Server) buildDefaultHTTPRouter() *mux.Router {
|
||||
return router
|
||||
}
|
||||
|
||||
func getRoute(any interface{}, rule string, value ...interface{}) (*mux.Route, error) {
|
||||
switch rule {
|
||||
case "PathStrip":
|
||||
rule = "Path"
|
||||
case "PathPrefixStrip":
|
||||
rule = "PathPrefix"
|
||||
func getRoute(serverRoute *serverRoute, route *types.Route) error {
|
||||
// ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
|
||||
// TODO: backwards compatibility with DEPRECATED rule.Value
|
||||
if len(route.Value) > 0 {
|
||||
route.Rule += ":" + route.Value
|
||||
log.Warnf("Value %s is DEPRECATED (will be removed in v1.0.0), please refer to the new frontend notation: https://github.com/containous/traefik/blob/master/docs/index.md#-frontends", route.Value)
|
||||
}
|
||||
inputs := make([]reflect.Value, len(value))
|
||||
for i := range value {
|
||||
inputs[i] = reflect.ValueOf(value[i])
|
||||
// ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
|
||||
|
||||
rules := Rules{route: serverRoute}
|
||||
newRoute, err := rules.Parse(route.Rule)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
method := reflect.ValueOf(any).MethodByName(rule)
|
||||
if method.IsValid() {
|
||||
return method.Call(inputs)[0].Interface().(*mux.Route), nil
|
||||
}
|
||||
return nil, errors.New("Method not found: " + rule)
|
||||
serverRoute.route = newRoute
|
||||
return nil
|
||||
}
|
||||
|
||||
func sortedFrontendNamesForConfig(configuration *types.Configuration) []string {
|
||||
keys := []string{}
|
||||
|
||||
for key := range configuration.Frontends {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
return keys
|
||||
}
|
||||
|
@@ -1,13 +1,41 @@
|
||||
[backends]{{range .Nodes}}
|
||||
[backends.backend-{{getBackend .}}.servers.server-{{.Node.Node | replace "." "-"}}-{{.Service.Port}}]
|
||||
url = "http://{{.Node.Address}}:{{.Service.Port}}"
|
||||
[backends]
|
||||
{{range $index, $node := .Nodes}}
|
||||
{{if ne (getAttribute "enable" $node.Service.Tags "true") "false"}}
|
||||
[backends.backend-{{getBackend $node}}.servers.{{getBackendName $node $index}}]
|
||||
url = "{{getAttribute "protocol" $node.Service.Tags "http"}}://{{getBackendAddress $node}}:{{$node.Service.Port}}"
|
||||
{{$weight := getAttribute "backend.weight" $node.Service.Tags ""}}
|
||||
{{with $weight}}
|
||||
weight = {{$weight}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
[frontends]{{range .Services}}
|
||||
[frontends.frontend-{{.}}]
|
||||
backend = "backend-{{.}}"
|
||||
passHostHeader = false
|
||||
[frontends.frontend-{{.}}.routes.route-host-{{.}}]
|
||||
rule = "Host"
|
||||
value = "{{getFrontendValue .}}"
|
||||
{{range .Services}}
|
||||
{{$service := .ServiceName}}
|
||||
{{$circuitBreaker := getAttribute "backend.circuitbreaker" .Attributes ""}}
|
||||
{{with $circuitBreaker}}
|
||||
[backends.backend-{{$service}}.circuitbreaker]
|
||||
expression = "{{$circuitBreaker}}"
|
||||
{{end}}
|
||||
|
||||
{{$loadBalancer := getAttribute "backend.loadbalancer" .Attributes ""}}
|
||||
{{with $loadBalancer}}
|
||||
[backends.backend-{{$service}}.loadbalancer]
|
||||
method = "{{$loadBalancer}}"
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
[frontends]
|
||||
{{range .Services}}
|
||||
[frontends.frontend-{{.ServiceName}}]
|
||||
backend = "backend-{{.ServiceName}}"
|
||||
passHostHeader = {{getAttribute "frontend.passHostHeader" .Attributes "false"}}
|
||||
{{$entryPoints := getAttribute "frontend.entrypoints" .Attributes ""}}
|
||||
{{with $entryPoints}}
|
||||
entrypoints = [{{range getEntryPoints $entryPoints}}
|
||||
"{{.}}",
|
||||
{{end}}]
|
||||
{{end}}
|
||||
[frontends.frontend-{{.ServiceName}}.routes.route-host-{{.ServiceName}}]
|
||||
rule = "{{getFrontendRule .}}"
|
||||
{{end}}
|
||||
|