Compare commits
329 Commits
v1.0.alpha
...
v1.0.0-rc1
Author | SHA1 | Date | |
---|---|---|---|
|
a9f9894f29 | ||
|
a6c360eeda | ||
|
01a4002169 | ||
|
8caaf317ae | ||
|
0e3c2ef10f | ||
|
db6c85d3d7 | ||
|
2bd95620a5 | ||
|
d8ad30f38a | ||
|
aad5f52968 | ||
|
f5d49f6657 | ||
|
53ae64e578 | ||
|
1a936b6aca | ||
|
4776fa1361 | ||
|
c5084fd025 | ||
|
cc2735f733 | ||
|
7f6b2b80f8 | ||
|
f64c2bc065 | ||
|
6752b49536 | ||
|
ab138e7df1 | ||
|
059da90a96 | ||
|
0821c7bdd9 | ||
|
89e00eb5a4 | ||
|
1a0f347023 | ||
|
1e27c2dabe | ||
|
629be45c4a | ||
|
e115e3c4e7 | ||
|
414fb1f406 | ||
|
fe0a8f3363 | ||
|
45589d5133 | ||
|
7804787e9e | ||
|
2e735f622f | ||
|
6accb90c47 | ||
|
e948a013cd | ||
|
b79535f369 | ||
|
ed3bcc6d9a | ||
|
0f23581f64 | ||
|
2af1e4b192 | ||
|
dc404b365f | ||
|
86f3891a2b | ||
|
86053ea54b | ||
|
938600ba95 | ||
|
80ab967d39 | ||
|
43acbaa702 | ||
|
5d6492e6f5 | ||
|
aeb9cc1732 | ||
|
fa25c8ef22 | ||
|
77a9613c3a | ||
|
153ab8f0fa | ||
|
f6c860afc0 | ||
|
d13b755df2 | ||
|
6bacbf6cac | ||
|
5923d22379 | ||
|
70494117d1 | ||
|
8210743dad | ||
|
895f3cc109 | ||
|
71f160dddc | ||
|
92abaa0d47 | ||
|
47710c1385 | ||
|
df3abcbc9a | ||
|
dbb7ad41e5 | ||
|
9773d4e409 | ||
|
993165fa66 | ||
|
c49f5dad05 | ||
|
c0bdedfed3 | ||
|
061107b65f | ||
|
7bf421f847 | ||
|
cb0c1d34a2 | ||
|
749b381f26 | ||
|
d89279d708 | ||
|
be209ed30c | ||
|
4a4ba2791d | ||
|
c61d9776e7 | ||
|
b5716abd3e | ||
|
b9bb78d04b | ||
|
8a39ee65cd | ||
|
301a463aeb | ||
|
d1b0bece47 | ||
|
63fd7d1d63 | ||
|
f4fb2518a1 | ||
|
ee486de947 | ||
|
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 | ||
|
dd160dc342 | ||
|
4a9e82903e | ||
|
1d040dbdd2 | ||
|
e4db9c72dd | ||
|
6308ce2740 | ||
|
87bad71bec | ||
|
50f09c8e4d | ||
|
bb1ecdd3c9 | ||
|
a2c3e6e405 | ||
|
cddbb44c75 | ||
|
7aa0c91401 | ||
|
6bfc849a24 | ||
|
ac4aa0d182 | ||
|
d9ffc39075 | ||
|
87e8393b07 | ||
|
1ab9c82dfb | ||
|
6e484e5c2d | ||
|
087b68e14d | ||
|
c313950891 | ||
|
7716d3377a | ||
|
0cbe34eef3 | ||
|
08d8c334a3 | ||
|
d75a151df3 | ||
|
10e223ede2 | ||
|
6a8bacf01c | ||
|
d4cc3900bd | ||
|
ab619a4a3f | ||
|
4c447985b6 | ||
|
eaadd2d0cd | ||
|
9830086790 | ||
|
8393746e02 | ||
|
2314ad9bf9 | ||
|
3af21612b6 | ||
|
7674a82801 | ||
|
d63d2a8a26 | ||
|
a458018aa2 | ||
|
33cde6aacd | ||
|
4ded2682d2 | ||
|
4042938556 | ||
|
0e683cc535 | ||
|
4923da7f4d | ||
|
11781087ca | ||
|
3063251d43 | ||
|
b42b170ad2 | ||
|
defbb44b35 | ||
|
a00eb81f03 | ||
|
a63d989a35 | ||
|
6c3c5578c6 | ||
|
122783e36b | ||
|
b84b95fe97 | ||
|
a99010b8c2 | ||
|
8954aa7118 | ||
|
3cf848958f | ||
|
1a5668377c | ||
|
dc10c56b35 | ||
|
331cd173ce | ||
|
1881d5eeed | ||
|
e0872b6157 | ||
|
63fb9c7135 | ||
|
9964654495 | ||
|
ae275c9e60 | ||
|
4277fe2fdb | ||
|
7acc2beae0 | ||
|
847deeac79 | ||
|
ac56c1310c | ||
|
7460b343fe | ||
|
ec16011e31 | ||
|
71b0e27517 | ||
|
60e9282f0a | ||
|
6cd35a50ce | ||
|
b35ad76ec6 | ||
|
54208f6fc3 | ||
|
6282bf33a0 | ||
|
a1c1958235 | ||
|
91b699fbe0 | ||
|
3a08655b06 | ||
|
9a9c8e5709 | ||
|
c7d34b54aa | ||
|
8d860c84c8 | ||
|
1dc086730e | ||
|
5d79e56d30 | ||
|
6e7677de79 | ||
|
fab6b8be3c | ||
|
f1c1eed437 | ||
|
348ab794c9 | ||
|
aacedcc4b3 | ||
|
786acc961a | ||
|
7adffdbd78 | ||
|
e3b519cdd8 | ||
|
e9c23195a0 | ||
|
c6c3af8099 | ||
|
07c077cf94 | ||
|
4ac18f1989 | ||
|
4ecb919787 | ||
|
4152bd5e26 | ||
|
a8cc26fd91 | ||
|
81cb00573f | ||
|
c22598c8ff | ||
|
bb3b9f61cd | ||
|
49cd7f799e | ||
|
8b334551d8 | ||
|
5ef6d53d00 | ||
|
901c9b29bc | ||
|
2d79c500df | ||
|
d3598021b7 | ||
|
31e0340959 | ||
|
cb46e8751b | ||
|
739a836c52 | ||
|
1cbe00d613 | ||
|
10d92ca176 | ||
|
257dbd188f | ||
|
38cc0579a6 | ||
|
a9c8cda5ec | ||
|
06654ff3a6 | ||
|
f1b62b45f4 | ||
|
8adadaa5d4 | ||
|
35070f7c1c | ||
|
a0b15a0efd | ||
|
b906e9361f | ||
|
ec2d7efe0e | ||
|
81e9fdfe75 | ||
|
465bb133c7 | ||
|
c15d7e03b4 | ||
|
5bfcfeb779 | ||
|
faa7fd0f05 | ||
|
ab50b10d1b | ||
|
b7a71edfcb | ||
|
587b17c120 | ||
|
c46ffed846 | ||
|
707b6f9a95 | ||
|
4bdc704a25 | ||
|
c0fd700904 | ||
|
72177c676e |
@@ -1,3 +1,5 @@
|
||||
dist/
|
||||
vendor/
|
||||
!dist/traefik
|
||||
site/
|
||||
**/*.test
|
||||
|
109
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
# Contributing
|
||||
|
||||
### Building
|
||||
|
||||
You need either [Docker](https://github.com/docker/docker) and `make`, or `go` and `glide` in order to build traefik.
|
||||
|
||||
#### Setting up your `go` environment
|
||||
|
||||
- You need `go` v1.5
|
||||
- You need to set `export GO15VENDOREXPERIMENT=1` environment variable
|
||||
- You need `go-bindata` to be able to use `go generate` command (needed to build) : `go get github.com/jteeuwen/go-bindata/...`.
|
||||
- If you clone Træfɪk into something like `~/go/src/github.com/traefik`, your `GOPATH` variable will have to be set to `~/go`: export `GOPATH=~/go`.
|
||||
|
||||
#### Using `Docker` and `Makefile`
|
||||
|
||||
You need to run the `binary` target. This will create binaries for Linux platform in the `dist` folder.
|
||||
|
||||
```bash
|
||||
$ make binary
|
||||
docker build -t "traefik-dev:no-more-godep-ever" -f build.Dockerfile .
|
||||
Sending build context to Docker daemon 295.3 MB
|
||||
Step 0 : FROM golang:1.5
|
||||
---> 8c6473912976
|
||||
Step 1 : RUN go get github.com/Masterminds/glide
|
||||
[...]
|
||||
docker run --rm -v "/var/run/docker.sock:/var/run/docker.sock" -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -v "/home/emile/dev/go/src/github.com/containous/traefik/"dist":/go/src/github.com/containous/traefik/"dist"" "traefik-dev:no-more-godep-ever" ./script/make.sh generate binary
|
||||
---> Making bundle: generate (in .)
|
||||
removed 'gen.go'
|
||||
|
||||
---> Making bundle: binary (in .)
|
||||
|
||||
$ ls dist/
|
||||
traefik*
|
||||
```
|
||||
|
||||
#### Using `glide`
|
||||
|
||||
The idea behind `glide` is the following :
|
||||
|
||||
- when checkout(ing) a project, **run `glide install`** to install
|
||||
(`go get …`) the dependencies in the `GOPATH`.
|
||||
- if you need another dependency, import and use it in
|
||||
the source, and **run `glide get github.com/Masterminds/cookoo`** to save it in
|
||||
`vendor` and add it to your `glide.yaml`.
|
||||
|
||||
```bash
|
||||
$ glide install
|
||||
# generate
|
||||
$ go generate
|
||||
# Simple go build
|
||||
$ go build
|
||||
# Using gox to build multiple platform
|
||||
$ gox "linux darwin" "386 amd64 arm" \
|
||||
-output="dist/traefik_{{.OS}}-{{.Arch}}"
|
||||
# run other commands like tests
|
||||
$ go test ./...
|
||||
ok _/home/vincent/src/github/vdemeester/traefik 0.004s
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
You can run unit tests using the `test-unit` target and the
|
||||
integration test using the `test-integration` target.
|
||||
|
||||
```bash
|
||||
$ make test-unit
|
||||
docker build -t "traefik-dev:your-feature-branch" -f build.Dockerfile .
|
||||
# […]
|
||||
docker run --rm -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -v "/home/vincent/src/github/vdemeester/traefik/dist:/go/src/github.com/containous/traefik/dist" "traefik-dev:your-feature-branch" ./script/make.sh generate test-unit
|
||||
---> Making bundle: generate (in .)
|
||||
removed 'gen.go'
|
||||
|
||||
---> Making bundle: test-unit (in .)
|
||||
+ go test -cover -coverprofile=cover.out .
|
||||
ok github.com/containous/traefik 0.005s coverage: 4.1% of statements
|
||||
|
||||
Test success
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
The [documentation site](http://docs.traefik.io/) is built with [mkdocs](http://mkdocs.org/)
|
||||
|
||||
First make sure you have python and pip installed
|
||||
|
||||
```
|
||||
$ python --version
|
||||
Python 2.7.2
|
||||
$ pip --version
|
||||
pip 1.5.2
|
||||
```
|
||||
|
||||
Then install mkdocs with pip
|
||||
|
||||
```
|
||||
$ pip install mkdocs
|
||||
```
|
||||
|
||||
To test documentaion localy run `mkdocs serve` in the root directory, this should start a server localy to preview your changes.
|
||||
|
||||
```
|
||||
$ mkdocs serve
|
||||
INFO - Building documentation...
|
||||
WARNING - Config value: 'theme'. Warning: The theme 'united' will be removed in an upcoming MkDocs release. See http://www.mkdocs.org/about/release-notes/ for more details
|
||||
INFO - Cleaning site directory
|
||||
[I 160505 22:31:24 server:281] Serving on http://127.0.0.1:8000
|
||||
[I 160505 22:31:24 handlers:59] Start watching changes
|
||||
[I 160505 22:31:24 handlers:61] Start detecting changes
|
||||
```
|
6
.gitignore
vendored
@@ -1,9 +1,15 @@
|
||||
/dist
|
||||
gen.go
|
||||
.idea
|
||||
.intellij
|
||||
log
|
||||
*.iml
|
||||
traefik
|
||||
traefik.toml
|
||||
*.test
|
||||
vendor/
|
||||
static/
|
||||
.vscode/
|
||||
site/
|
||||
*.log
|
||||
*.exe
|
||||
|
10
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
- repo: git://github.com/pre-commit/pre-commit-hooks
|
||||
sha: 44e1753f98b0da305332abe26856c3e621c5c439
|
||||
hooks:
|
||||
- id: detect-private-key
|
||||
- repo: git://github.com/containous/pre-commit-hooks
|
||||
sha: 35e641b5107671e94102b0ce909648559e568d61
|
||||
hooks:
|
||||
- id: goFmt
|
||||
- id: goLint
|
||||
- id: goErrcheck
|
31
.travis.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
branches:
|
||||
env:
|
||||
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: $TRAVIS_TAG
|
||||
matrix:
|
||||
- DOCKER_VERSION=1.9.1
|
||||
- DOCKER_VERSION=1.10.1
|
||||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
install:
|
||||
- sudo service docker stop
|
||||
- sudo curl https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION} -o /usr/bin/docker
|
||||
- sudo chmod +x /usr/bin/docker
|
||||
- sudo service docker start
|
||||
- sleep 5
|
||||
- docker version
|
||||
- pip install --user mkdocs
|
||||
- pip install --user pymdown-extensions
|
||||
before_script:
|
||||
- make validate
|
||||
- make binary
|
||||
script:
|
||||
- make test-unit
|
||||
- make test-integration
|
||||
- make crossbinary
|
||||
- make image
|
||||
after_success:
|
||||
- make deploy
|
BIN
.travis/traefik.id_rsa.enc
Normal file
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Emile Vauge, emile@vauge.com
|
||||
Copyright (c) 2016 Containous SAS, Emile Vauge, emile@vauge.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
THE SOFTWARE.
|
||||
|
65
Makefile
@@ -4,63 +4,61 @@ TRAEFIK_ENVS := \
|
||||
-e OS_ARCH_ARG \
|
||||
-e OS_PLATFORM_ARG \
|
||||
-e TESTFLAGS \
|
||||
-e CIRCLECI
|
||||
-e VERBOSE \
|
||||
-e VERSION
|
||||
|
||||
SRCS = $(shell git ls-files '*.go' | grep -v '^external/')
|
||||
|
||||
BIND_DIR := "dist"
|
||||
TRAEFIK_MOUNT := -v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/emilevauge/traefik/$(BIND_DIR)"
|
||||
TRAEFIK_MOUNT := -v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/containous/traefik/$(BIND_DIR)"
|
||||
|
||||
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||
TRAEFIK_DEV_IMAGE := traefik-dev$(if $(GIT_BRANCH),:$(GIT_BRANCH))
|
||||
REPONAME := $(shell echo $(REPO) | tr '[:upper:]' '[:lower:]')
|
||||
TRAEFIK_IMAGE := $(if $(REPONAME),$(REPONAME),"emilevauge/traefik")
|
||||
TRAEFIK_IMAGE := $(if $(REPONAME),$(REPONAME),"containous/traefik")
|
||||
INTEGRATION_OPTS := $(if $(MAKE_DOCKER_HOST),-e "DOCKER_HOST=$(MAKE_DOCKER_HOST)", -v "/var/run/docker.sock:/var/run/docker.sock")
|
||||
|
||||
DOCKER_RUN_TRAEFIK := docker run $(if $(CIRCLECI),,--rm) $(INTEGRATION_OPTS) -it $(TRAEFIK_ENVS) $(TRAEFIK_MOUNT) "$(TRAEFIK_DEV_IMAGE)"
|
||||
DOCKER_BUILD_ARGS := $(if $(DOCKER_VERSION), "--build-arg=DOCKER_VERSION=$(DOCKER_VERSION)",)
|
||||
DOCKER_RUN_TRAEFIK := docker run $(INTEGRATION_OPTS) -it $(TRAEFIK_ENVS) $(TRAEFIK_MOUNT) "$(TRAEFIK_DEV_IMAGE)"
|
||||
|
||||
print-%: ; @echo $*=$($*)
|
||||
|
||||
default: binary
|
||||
|
||||
all: 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: build
|
||||
binary: generate-webui build ## build the linux binary
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate binary
|
||||
|
||||
crossbinary: 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
|
||||
$(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
|
||||
validate: build ## validate gofmt, golint and go vet
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt validate-govet validate-golint
|
||||
|
||||
build: dist
|
||||
docker build -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile .
|
||||
docker build $(DOCKER_BUILD_ARGS) -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile .
|
||||
|
||||
build-webui:
|
||||
docker build -t traefik-webui -f webui/Dockerfile 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:
|
||||
@@ -70,3 +68,22 @@ run-dev:
|
||||
go generate
|
||||
go build
|
||||
./traefik
|
||||
|
||||
generate-webui: build-webui
|
||||
if [ ! -d "static" ]; then \
|
||||
mkdir -p static; \
|
||||
docker run --rm -v "$$PWD/static":'/src/static' traefik-webui gulp; \
|
||||
echo 'For more informations show `webui/readme.md`' > $$PWD/static/DONT-EDIT-FILES-IN-THIS-DIRECTORY.md; \
|
||||
fi
|
||||
|
||||
lint:
|
||||
script/validate-golint
|
||||
|
||||
fmt:
|
||||
gofmt -s -l -w $(SRCS)
|
||||
|
||||
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)
|
||||
|
158
README.md
@@ -1,38 +1,73 @@
|
||||

|
||||
___
|
||||
|
||||
[](https://circleci.com/gh/emilevauge/traefik)
|
||||
[](https://github.com/EmileVauge/traefik/blob/master/LICENSE.md)
|
||||
<p align="center">
|
||||
<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/), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Consul](https://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](http://docs.traefik.io/benchmarks)
|
||||
- No dependency hell, single binary made with go
|
||||
- Simple json Rest API
|
||||
- Simple TOML file configuration
|
||||
- Rest API
|
||||
- Multiple backends supported: Docker, Mesos/Marathon, Consul, Etcd, and more to come
|
||||
- Watchers for backends, can listen change in backends to apply a new configuration automatically
|
||||
- Hot-reloading of configuration. No need to restart the process
|
||||
- Graceful shutdown http connections during hot-reloads
|
||||
- Graceful shutdown http connections
|
||||
- Circuit breakers on backends
|
||||
- Round Robin, rebalancer load-balancers
|
||||
- Rest Metrics
|
||||
- Tiny docker image included
|
||||
- [Tiny](https://imagelayers.io/?images=traefik) [official](https://hub.docker.com/r/_/traefik/) docker image included
|
||||
- SSL backends support
|
||||
- SSL frontend support
|
||||
- SSL frontend support (with SNI)
|
||||
- Clean AngularJS Web UI
|
||||
- Websocket support
|
||||
- HTTP/2 support
|
||||
- Retry request if network error
|
||||
- [Let's Encrypt](https://letsencrypt.org) support (Automatic HTTPS)
|
||||
|
||||
## Demo
|
||||
|
||||
Here is a demo of Træfɪk using Docker backend, showing a load-balancing between two servers, hot reloading of configuration, and graceful shutdown.
|
||||
|
||||
[](https://asciinema.org/a/4tcyde7riou5vxulo6my3mtko)
|
||||
Here is a talk (in french) given by [Emile Vauge](https://github.com/emilevauge) at the [Devoxx France 2016](http://www.devoxx.fr) conference.
|
||||
You will learn fundamental Træfɪk features and see some demos with Docker, Mesos/Marathon and Lets'Encrypt.
|
||||
|
||||
[](http://www.youtube.com/watch?v=QvAz9mVx5TI)
|
||||
|
||||
## Web UI
|
||||
|
||||
@@ -43,114 +78,73 @@ You can access to a simple HTML frontend of Træfik.
|
||||
|
||||
## Plumbing
|
||||
|
||||
- [Oxy](https://github.com/mailgun/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
|
||||
- [Lego](https://github.com/xenolf/lego): the best [Let's Encrypt](https://letsencrypt.org) library in go
|
||||
|
||||
## Quick start
|
||||
|
||||
- The simple way: grab the latest binary from the [releases](https://github.com/emilevauge/traefik/releases) page and just run it with the [sample configuration file](https://raw.githubusercontent.com/EmileVauge/traefik/master/traefik.sample.toml):
|
||||
- The simple way: grab the latest binary from the [releases](https://github.com/containous/traefik/releases) page and just run it with the [sample configuration file](https://raw.githubusercontent.com/containous/traefik/master/traefik.sample.toml):
|
||||
|
||||
```shell
|
||||
./traefik traefik.toml
|
||||
./traefik --configFile=traefik.toml
|
||||
```
|
||||
|
||||
- Use the tiny Docker image:
|
||||
|
||||
```shell
|
||||
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/traefik.toml emilevauge/traefik
|
||||
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik
|
||||
```
|
||||
|
||||
- From sources:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/EmileVauge/traefik
|
||||
git clone https://github.com/containous/traefik
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
You can find the complete documentation [here](docs/index.md).
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Refer to the [benchmarks section](docs/index.md#benchmarks) in the documentation.
|
||||
You can find the complete documentation [here](https://docs.traefik.io).
|
||||
|
||||
## Contributing
|
||||
|
||||
### Building
|
||||
Please refer to [this section](.github/CONTRIBUTING.md).
|
||||
|
||||
You need either [Docker](https://github.com/docker/docker) and `make`, or `go` and `glide` in order to build traefik.
|
||||
## Support
|
||||
|
||||
#### Setting up your `go` environment
|
||||
- You need `go` v1.5
|
||||
- You need to set `export GO15VENDOREXPERIMENT=1` environment variable
|
||||
- You need `go-bindata` to be able to use `go generate` command (needed to build) : `go get github.com/jteeuwen/go-bindata/...`.
|
||||
- If you clone Træfɪk into something like `~/go/src/github.com/traefik`, your `GOPATH` variable will have to be set to `~/go`: export `GOPATH=~/go`.
|
||||
You can join [](https://traefik.herokuapp.com) to get basic support.
|
||||
If you prefer a commercial support, please contact [containo.us](https://containo.us) by mail: <mailto:support@containo.us>.
|
||||
|
||||
#### Using `Docker` and `Makefile`
|
||||
## Træfɪk here and there
|
||||
|
||||
You need to run the `binary` target. This will create binaries for
|
||||
linux platform in the `dist` folder.
|
||||
These projects use Træfɪk internally. If your company uses Træfɪk, we would be glad to get your feedback :) Contact us on [](https://traefik.herokuapp.com)
|
||||
|
||||
```bash
|
||||
$ make binary
|
||||
docker build -t "traefik-dev:no-more-godep-ever" -f build.Dockerfile .
|
||||
Sending build context to Docker daemon 295.3 MB
|
||||
Step 0 : FROM golang:1.5
|
||||
---> 8c6473912976
|
||||
Step 1 : RUN go get github.com/Masterminds/glide
|
||||
[...]
|
||||
docker run --rm -v "/var/run/docker.sock:/var/run/docker.sock" -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -v "/home/emile/dev/go/src/github.com/emilevauge/traefik/"dist":/go/src/github.com/emilevauge/traefik/"dist"" "traefik-dev:no-more-godep-ever" ./script/make.sh generate binary
|
||||
---> Making bundle: generate (in .)
|
||||
removed 'gen.go'
|
||||
- Project [Mantl](https://mantl.io/) from Cisco
|
||||
|
||||
---> Making bundle: binary (in .)
|
||||

|
||||
> Mantl is a modern platform for rapidly deploying globally distributed services. A container orchestrator, docker, a network stack, something to pool your logs, something to monitor health, a sprinkle of service discovery and some automation.
|
||||
|
||||
$ ls dist/
|
||||
traefik*
|
||||
```
|
||||
- Project [Apollo](http://capgemini.github.io/devops/apollo/) from Cap Gemini
|
||||
|
||||
#### Using `glide`
|
||||

|
||||
> Apollo is an open source project to aid with building and deploying IAAS and PAAS services. It is particularly geared towards managing containerized applications across multiple hosts, and big data type workloads. Apollo leverages other open source components to provide basic mechanisms for deployment, maintenance, and scaling of infrastructure and applications.
|
||||
|
||||
The idea behind `glide` is the following :
|
||||
## Partners
|
||||
|
||||
- when checkout(ing) a project, **run `glide up`** to install
|
||||
(`go get …`) the dependencies in the `GOPATH`.
|
||||
- if you need another dependency, import and use it in
|
||||
the source, and **run `glide get github.com/Masterminds/cookoo`** to save it in
|
||||
`vendor` and add it to your `glide.yaml`.
|
||||
[](https://zenika.com)
|
||||
|
||||
```bash
|
||||
$ glide up --update-vendored
|
||||
# generate
|
||||
$ go generate
|
||||
# Simple go build
|
||||
$ go build
|
||||
# Using gox to build multiple platform
|
||||
$ gox "linux darwin" "386 amd64 arm" \
|
||||
-output="dist/traefik_{{.OS}}-{{.Arch}}"
|
||||
# run other commands like tests
|
||||
$ go test ./...
|
||||
ok _/home/vincent/src/github/vdemeester/traefik 0.004s
|
||||
```
|
||||
Zenika is one of the leading providers of professional Open Source services and agile methodologies in
|
||||
Europe. We provide consulting, development, training and support for the world’s leading Open Source
|
||||
software products.
|
||||
|
||||
### Tests
|
||||
|
||||
You can run unit tests using the `test-unit` target and the
|
||||
integration test using the `test-integration` target.
|
||||
[](https://aster.is)
|
||||
|
||||
```bash
|
||||
$ make test-unit
|
||||
docker build -t "traefik-dev:your-feature-branch" -f build.Dockerfile .
|
||||
# […]
|
||||
docker run --rm -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -v "/home/vincent/src/github/vdemeester/traefik/dist:/go/src/github.com/emilevauge/traefik/dist" "traefik-dev:your-feature-branch" ./script/make.sh generate test-unit
|
||||
---> Making bundle: generate (in .)
|
||||
removed 'gen.go'
|
||||
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.
|
||||
.
|
||||
|
||||
---> Making bundle: test-unit (in .)
|
||||
+ go test -cover -coverprofile=cover.out .
|
||||
ok github.com/emilevauge/traefik 0.005s coverage: 4.1% of statements
|
||||
## Credits
|
||||
|
||||
Test success
|
||||
```
|
||||
Kudos to [Peka](http://peka.byethost11.com/photoblog/) for his awesome work on the logo 
|
459
acme/acme.go
Normal file
@@ -0,0 +1,459 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/xenolf/lego/acme"
|
||||
"io/ioutil"
|
||||
fmtlog "log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Account is used to store lets encrypt registration info
|
||||
type Account struct {
|
||||
Email string
|
||||
Registration *acme.RegistrationResource
|
||||
PrivateKey []byte
|
||||
DomainsCertificate DomainsCertificates
|
||||
}
|
||||
|
||||
// GetEmail returns email
|
||||
func (a Account) GetEmail() string {
|
||||
return a.Email
|
||||
}
|
||||
|
||||
// GetRegistration returns lets encrypt registration resource
|
||||
func (a Account) GetRegistration() *acme.RegistrationResource {
|
||||
return a.Registration
|
||||
}
|
||||
|
||||
// GetPrivateKey returns private key
|
||||
func (a Account) GetPrivateKey() crypto.PrivateKey {
|
||||
if privateKey, err := x509.ParsePKCS1PrivateKey(a.PrivateKey); err == nil {
|
||||
return privateKey
|
||||
}
|
||||
log.Errorf("Cannot unmarshall private key %+v", a.PrivateKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Certificate is used to store certificate info
|
||||
type Certificate struct {
|
||||
Domain string
|
||||
CertURL string
|
||||
CertStableURL string
|
||||
PrivateKey []byte
|
||||
Certificate []byte
|
||||
}
|
||||
|
||||
// DomainsCertificates stores a certificate for multiple domains
|
||||
type DomainsCertificates struct {
|
||||
Certs []*DomainsCertificate
|
||||
lock *sync.RWMutex
|
||||
}
|
||||
|
||||
func (dc *DomainsCertificates) init() error {
|
||||
if dc.lock == nil {
|
||||
dc.lock = &sync.RWMutex{}
|
||||
}
|
||||
dc.lock.Lock()
|
||||
defer dc.lock.Unlock()
|
||||
for _, domainsCertificate := range dc.Certs {
|
||||
tlsCert, err := tls.X509KeyPair(domainsCertificate.Certificate.Certificate, domainsCertificate.Certificate.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
domainsCertificate.tlsCert = &tlsCert
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *DomainsCertificates) renewCertificates(acmeCert *Certificate, domain Domain) error {
|
||||
dc.lock.Lock()
|
||||
defer dc.lock.Unlock()
|
||||
|
||||
for _, domainsCertificate := range dc.Certs {
|
||||
if reflect.DeepEqual(domain, domainsCertificate.Domains) {
|
||||
domainsCertificate.Certificate = acmeCert
|
||||
tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
domainsCertificate.tlsCert = &tlsCert
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("Certificate to renew not found for domain " + domain.Main)
|
||||
}
|
||||
|
||||
func (dc *DomainsCertificates) addCertificateForDomains(acmeCert *Certificate, domain Domain) (*DomainsCertificate, error) {
|
||||
dc.lock.Lock()
|
||||
defer dc.lock.Unlock()
|
||||
|
||||
tlsCert, err := tls.X509KeyPair(acmeCert.Certificate, acmeCert.PrivateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert := DomainsCertificate{Domains: domain, Certificate: acmeCert, tlsCert: &tlsCert}
|
||||
dc.Certs = append(dc.Certs, &cert)
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
func (dc *DomainsCertificates) getCertificateForDomain(domainToFind string) (*DomainsCertificate, bool) {
|
||||
dc.lock.RLock()
|
||||
defer dc.lock.RUnlock()
|
||||
for _, domainsCertificate := range dc.Certs {
|
||||
domains := []string{}
|
||||
domains = append(domains, domainsCertificate.Domains.Main)
|
||||
domains = append(domains, domainsCertificate.Domains.SANs...)
|
||||
for _, domain := range domains {
|
||||
if domain == domainToFind {
|
||||
return domainsCertificate, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (dc *DomainsCertificates) exists(domainToFind Domain) (*DomainsCertificate, bool) {
|
||||
dc.lock.RLock()
|
||||
defer dc.lock.RUnlock()
|
||||
for _, domainsCertificate := range dc.Certs {
|
||||
if reflect.DeepEqual(domainToFind, domainsCertificate.Domains) {
|
||||
return domainsCertificate, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// DomainsCertificate contains a certificate for multiple domains
|
||||
type DomainsCertificate struct {
|
||||
Domains Domain
|
||||
Certificate *Certificate
|
||||
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 `description:"Email address used for registration"`
|
||||
Domains []Domain `description:"SANs (alternative domains) to each main domain using format: --acme.domains='main.com,san1.com,san2.com' --acme.domains='main.net,san1.net,san2.net'"`
|
||||
StorageFile string `description:"File used for certificates storage."`
|
||||
OnDemand bool `description:"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."`
|
||||
CAServer string `description:"CA server to use."`
|
||||
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
|
||||
storageLock sync.RWMutex
|
||||
}
|
||||
|
||||
//Domains parse []Domain
|
||||
type Domains []Domain
|
||||
|
||||
//Set []Domain
|
||||
func (ds *Domains) Set(str string) error {
|
||||
fargs := func(c rune) bool {
|
||||
return c == ',' || c == ';'
|
||||
}
|
||||
// get function
|
||||
slice := strings.FieldsFunc(str, fargs)
|
||||
if len(slice) < 1 {
|
||||
return fmt.Errorf("Parse error ACME.Domain. Imposible to parse %s", str)
|
||||
}
|
||||
d := Domain{
|
||||
Main: slice[0],
|
||||
SANs: []string{},
|
||||
}
|
||||
if len(slice) > 1 {
|
||||
d.SANs = slice[1:]
|
||||
}
|
||||
*ds = append(*ds, d)
|
||||
return nil
|
||||
}
|
||||
|
||||
//Get []Domain
|
||||
func (ds *Domains) Get() interface{} { return []Domain(*ds) }
|
||||
|
||||
//String returns []Domain in string
|
||||
func (ds *Domains) String() string { return fmt.Sprintf("%+v", *ds) }
|
||||
|
||||
//SetValue sets []Domain into the parser
|
||||
func (ds *Domains) SetValue(val interface{}) {
|
||||
*ds = Domains(val.([]Domain))
|
||||
}
|
||||
|
||||
// Domain holds a domain name with SANs
|
||||
type Domain struct {
|
||||
Main string
|
||||
SANs []string
|
||||
}
|
||||
|
||||
// CreateConfig creates a tls.config from using ACME configuration
|
||||
func (a *ACME) CreateConfig(tlsConfig *tls.Config, CheckOnDemandDomain func(domain string) bool) error {
|
||||
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
|
||||
|
||||
if len(a.StorageFile) == 0 {
|
||||
return errors.New("Empty StorageFile, please provide a filename for certs storage")
|
||||
}
|
||||
|
||||
log.Debugf("Generating default certificate...")
|
||||
if len(tlsConfig.Certificates) == 0 {
|
||||
// no certificates in TLS config, so we add a default one
|
||||
cert, err := generateDefaultCertificate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tlsConfig.Certificates = append(tlsConfig.Certificates, *cert)
|
||||
}
|
||||
var account *Account
|
||||
var needRegister bool
|
||||
|
||||
// if certificates in storage, load them
|
||||
if fileInfo, err := os.Stat(a.StorageFile); err == nil && fileInfo.Size() != 0 {
|
||||
log.Infof("Loading ACME certificates...")
|
||||
// load account
|
||||
account, err = a.loadAccount(a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
log.Infof("Generating ACME Account...")
|
||||
// Create a user. New accounts need an email and private key to start
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account = &Account{
|
||||
Email: a.Email,
|
||||
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
}
|
||||
account.DomainsCertificate = DomainsCertificates{Certs: []*DomainsCertificate{}, lock: &sync.RWMutex{}}
|
||||
needRegister = true
|
||||
}
|
||||
|
||||
client, err := a.buildACMEClient(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
|
||||
wrapperChallengeProvider := newWrapperChallengeProvider()
|
||||
client.SetChallengeProvider(acme.TLSSNI01, wrapperChallengeProvider)
|
||||
|
||||
if needRegister {
|
||||
// New users will need to register; be sure to save it
|
||||
reg, err := client.Register()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.Registration = reg
|
||||
}
|
||||
|
||||
// The client has a URL to the current Let's Encrypt Subscriber
|
||||
// Agreement. The user will need to agree to it.
|
||||
err = client.AgreeToTOS()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
safe.Go(func() {
|
||||
a.retrieveCertificates(client, account)
|
||||
})
|
||||
|
||||
tlsConfig.GetCertificate = func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if challengeCert, ok := wrapperChallengeProvider.getCertificate(clientHello.ServerName); ok {
|
||||
return challengeCert, nil
|
||||
}
|
||||
if domainCert, ok := account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok {
|
||||
return domainCert.tlsCert, nil
|
||||
}
|
||||
if a.OnDemand {
|
||||
if CheckOnDemandDomain != nil && !CheckOnDemandDomain(clientHello.ServerName) {
|
||||
return nil, nil
|
||||
}
|
||||
return a.loadCertificateOnDemand(client, account, clientHello)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
safe.Go(func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
|
||||
if err := a.renewCertificates(client, account); err != nil {
|
||||
log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ACME) retrieveCertificates(client *acme.Client, account *Account) {
|
||||
log.Infof("Retrieving ACME certificates...")
|
||||
for _, domain := range a.Domains {
|
||||
// check if cert isn't already loaded
|
||||
if _, exists := account.DomainsCertificate.exists(domain); !exists {
|
||||
domains := []string{}
|
||||
domains = append(domains, domain.Main)
|
||||
domains = append(domains, domain.SANs...)
|
||||
certificateResource, err := a.getDomainsCertificates(client, domains)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error())
|
||||
continue
|
||||
}
|
||||
_, err = account.DomainsCertificate.addCertificateForDomains(certificateResource, domain)
|
||||
if err != nil {
|
||||
log.Errorf("Error adding ACME certificate for domain %s: %s", domains, err.Error())
|
||||
continue
|
||||
}
|
||||
if err = a.saveAccount(account); err != nil {
|
||||
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Infof("Retrieved ACME certificates")
|
||||
}
|
||||
|
||||
func (a *ACME) renewCertificates(client *acme.Client, account *Account) error {
|
||||
for _, certificateResource := range account.DomainsCertificate.Certs {
|
||||
if certificateResource.needRenew() {
|
||||
log.Debugf("Renewing certificate %+v", certificateResource.Domains)
|
||||
renewedCert, err := client.RenewCertificate(acme.CertificateResource{
|
||||
Domain: certificateResource.Certificate.Domain,
|
||||
CertURL: certificateResource.Certificate.CertURL,
|
||||
CertStableURL: certificateResource.Certificate.CertStableURL,
|
||||
PrivateKey: certificateResource.Certificate.PrivateKey,
|
||||
Certificate: certificateResource.Certificate.Certificate,
|
||||
}, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Debugf("Renewed certificate %+v", certificateResource.Domains)
|
||||
renewedACMECert := &Certificate{
|
||||
Domain: renewedCert.Domain,
|
||||
CertURL: renewedCert.CertURL,
|
||||
CertStableURL: renewedCert.CertStableURL,
|
||||
PrivateKey: renewedCert.PrivateKey,
|
||||
Certificate: renewedCert.Certificate,
|
||||
}
|
||||
err = account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = a.saveAccount(account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *ACME) buildACMEClient(Account *Account) (*acme.Client, error) {
|
||||
caServer := "https://acme-v01.api.letsencrypt.org/directory"
|
||||
if len(a.CAServer) > 0 {
|
||||
caServer = a.CAServer
|
||||
}
|
||||
client, err := acme.NewClient(caServer, Account, acme.RSA4096)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (a *ACME) loadCertificateOnDemand(client *acme.Client, Account *Account, clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if certificateResource, ok := Account.DomainsCertificate.getCertificateForDomain(clientHello.ServerName); ok {
|
||||
return certificateResource.tlsCert, nil
|
||||
}
|
||||
Certificate, err := a.getDomainsCertificates(client, []string{clientHello.ServerName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Debugf("Got certificate on demand for domain %s", clientHello.ServerName)
|
||||
cert, err := Account.DomainsCertificate.addCertificateForDomains(Certificate, Domain{Main: clientHello.ServerName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = a.saveAccount(Account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cert.tlsCert, nil
|
||||
}
|
||||
|
||||
func (a *ACME) loadAccount(acmeConfig *ACME) (*Account, error) {
|
||||
a.storageLock.RLock()
|
||||
defer a.storageLock.RUnlock()
|
||||
Account := Account{
|
||||
DomainsCertificate: DomainsCertificates{},
|
||||
}
|
||||
file, err := ioutil.ReadFile(acmeConfig.StorageFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(file, &Account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = Account.DomainsCertificate.init()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("Loaded ACME config from storage %s", acmeConfig.StorageFile)
|
||||
return &Account, nil
|
||||
}
|
||||
|
||||
func (a *ACME) saveAccount(Account *Account) error {
|
||||
a.storageLock.Lock()
|
||||
defer a.storageLock.Unlock()
|
||||
// write account to file
|
||||
data, err := json.MarshalIndent(Account, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(a.StorageFile, data, 0644)
|
||||
}
|
||||
|
||||
func (a *ACME) getDomainsCertificates(client *acme.Client, domains []string) (*Certificate, error) {
|
||||
log.Debugf("Loading ACME certificates %s...", domains)
|
||||
bundle := true
|
||||
certificate, failures := client.ObtainCertificate(domains, bundle, nil)
|
||||
if len(failures) > 0 {
|
||||
log.Error(failures)
|
||||
return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures)
|
||||
}
|
||||
log.Debugf("Loaded ACME certificates %s", domains)
|
||||
return &Certificate{
|
||||
Domain: certificate.Domain,
|
||||
CertURL: certificate.CertURL,
|
||||
CertStableURL: certificate.CertStableURL,
|
||||
PrivateKey: certificate.PrivateKey,
|
||||
Certificate: certificate.Certificate,
|
||||
}, nil
|
||||
}
|
61
acme/acme_test.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDomainsSet(t *testing.T) {
|
||||
checkMap := map[string]Domains{
|
||||
"": {},
|
||||
"foo.com": {Domain{Main: "foo.com", SANs: []string{}}},
|
||||
"foo.com,bar.net": {Domain{Main: "foo.com", SANs: []string{"bar.net"}}},
|
||||
"foo.com,bar1.net,bar2.net,bar3.net": {Domain{Main: "foo.com", SANs: []string{"bar1.net", "bar2.net", "bar3.net"}}},
|
||||
}
|
||||
for in, check := range checkMap {
|
||||
ds := Domains{}
|
||||
ds.Set(in)
|
||||
if !reflect.DeepEqual(check, ds) {
|
||||
t.Errorf("Expected %+v\nGo %+v", check, ds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDomainsSetAppend(t *testing.T) {
|
||||
inSlice := []string{
|
||||
"",
|
||||
"foo1.com",
|
||||
"foo2.com,bar.net",
|
||||
"foo3.com,bar1.net,bar2.net,bar3.net",
|
||||
}
|
||||
checkSlice := []Domains{
|
||||
{},
|
||||
{
|
||||
Domain{
|
||||
Main: "foo1.com",
|
||||
SANs: []string{}}},
|
||||
{
|
||||
Domain{
|
||||
Main: "foo1.com",
|
||||
SANs: []string{}},
|
||||
Domain{
|
||||
Main: "foo2.com",
|
||||
SANs: []string{"bar.net"}}},
|
||||
{
|
||||
Domain{
|
||||
Main: "foo1.com",
|
||||
SANs: []string{}},
|
||||
Domain{
|
||||
Main: "foo2.com",
|
||||
SANs: []string{"bar.net"}},
|
||||
Domain{Main: "foo3.com",
|
||||
SANs: []string{"bar1.net", "bar2.net", "bar3.net"}}},
|
||||
}
|
||||
ds := Domains{}
|
||||
for i, in := range inSlice {
|
||||
ds.Set(in)
|
||||
if !reflect.DeepEqual(checkSlice[i], ds) {
|
||||
t.Errorf("Expected %s %+v\nGo %+v", in, checkSlice[i], ds)
|
||||
}
|
||||
}
|
||||
}
|
56
acme/challengeProvider.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"sync"
|
||||
|
||||
"crypto/x509"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
type wrapperChallengeProvider struct {
|
||||
challengeCerts map[string]*tls.Certificate
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func newWrapperChallengeProvider() *wrapperChallengeProvider {
|
||||
return &wrapperChallengeProvider{
|
||||
challengeCerts: map[string]*tls.Certificate{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *wrapperChallengeProvider) getCertificate(domain string) (cert *tls.Certificate, exists bool) {
|
||||
c.lock.RLock()
|
||||
defer c.lock.RUnlock()
|
||||
if cert, ok := c.challengeCerts[domain]; ok {
|
||||
return cert, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *wrapperChallengeProvider) Present(domain, token, keyAuth string) error {
|
||||
cert, err := acme.TLSSNI01ChallengeCert(keyAuth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
for i := range cert.Leaf.DNSNames {
|
||||
c.challengeCerts[cert.Leaf.DNSNames[i]] = &cert
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (c *wrapperChallengeProvider) CleanUp(domain, token, keyAuth string) error {
|
||||
c.lock.Lock()
|
||||
defer c.lock.Unlock()
|
||||
delete(c.challengeCerts, domain)
|
||||
return nil
|
||||
}
|
78
acme/crypto.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
func generateDefaultCertificate() (*tls.Certificate, error) {
|
||||
rsaPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rsaPrivPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaPrivKey)})
|
||||
|
||||
randomBytes := make([]byte, 100)
|
||||
_, err = rand.Read(randomBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zBytes := sha256.Sum256(randomBytes)
|
||||
z := hex.EncodeToString(zBytes[:sha256.Size])
|
||||
domain := fmt.Sprintf("%s.%s.traefik.default", z[:32], z[32:])
|
||||
tempCertPEM, err := generatePemCert(rsaPrivKey, domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &certificate, nil
|
||||
}
|
||||
func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) {
|
||||
derBytes, err := generateDerCert(privKey, time.Time{}, domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil
|
||||
}
|
||||
|
||||
func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) {
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if expiration.IsZero() {
|
||||
expiration = time.Now().Add(365)
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "TRAEFIK DEFAULT CERT",
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: expiration,
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment,
|
||||
BasicConstraintsValid: true,
|
||||
DNSNames: []string{domain},
|
||||
}
|
||||
|
||||
return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey)
|
||||
}
|
12
adapters.go
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// OxyLogger implements oxy Logger interface with logrus.
|
||||
@@ -24,19 +23,12 @@ 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) {
|
||||
http.NotFound(w, r)
|
||||
//templatesRenderer.HTML(w, http.StatusNotFound, "notFound", nil)
|
||||
}
|
||||
|
||||
// LoadDefaultConfig returns a default gorrilla.mux router from the specified configuration.
|
||||
func LoadDefaultConfig(globalConfiguration *GlobalConfiguration) *mux.Router {
|
||||
router := mux.NewRouter()
|
||||
router.NotFoundHandler = http.HandlerFunc(notFoundHandler)
|
||||
return router
|
||||
}
|
||||
|
@@ -1,16 +1,12 @@
|
||||
FROM golang:1.5
|
||||
FROM golang:1.6.2
|
||||
|
||||
RUN go get github.com/Masterminds/glide
|
||||
RUN go get github.com/mitchellh/gox
|
||||
RUN go get github.com/tcnksm/ghr
|
||||
RUN go get github.com/jteeuwen/go-bindata/...
|
||||
RUN go get github.com/golang/lint/golint
|
||||
RUN go get github.com/Masterminds/glide \
|
||||
&& go get github.com/jteeuwen/go-bindata/... \
|
||||
&& go get github.com/golang/lint/golint \
|
||||
&& go get github.com/kisielk/errcheck
|
||||
|
||||
# Which docker version to test on
|
||||
ENV DOCKER_VERSION 1.6.2
|
||||
|
||||
# enable GO15VENDOREXPERIMENT
|
||||
ENV GO15VENDOREXPERIMENT 1
|
||||
ARG DOCKER_VERSION=1.10.1
|
||||
|
||||
# Download docker
|
||||
RUN set -ex; \
|
||||
@@ -20,9 +16,10 @@ RUN set -ex; \
|
||||
# Set the default Docker to be run
|
||||
RUN ln -s /usr/local/bin/docker-${DOCKER_VERSION} /usr/local/bin/docker
|
||||
|
||||
WORKDIR /go/src/github.com/emilevauge/traefik
|
||||
WORKDIR /go/src/github.com/containous/traefik
|
||||
|
||||
COPY glide.yaml glide.yaml
|
||||
RUN glide up
|
||||
COPY glide.lock glide.lock
|
||||
RUN glide install
|
||||
|
||||
COPY . /go/src/github.com/emilevauge/traefik
|
||||
COPY . /go/src/github.com/containous/traefik
|
||||
|
318
configuration.go
@@ -1,32 +1,200 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
fmtlog "log"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/containous/traefik/acme"
|
||||
"github.com/containous/traefik/provider"
|
||||
"github.com/containous/traefik/types"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/emilevauge/traefik/provider"
|
||||
"github.com/emilevauge/traefik/types"
|
||||
)
|
||||
|
||||
// TraefikConfiguration holds GlobalConfiguration and other stuff
|
||||
type TraefikConfiguration struct {
|
||||
GlobalConfiguration
|
||||
ConfigFile string `short:"c" description:"Configuration file to use (TOML)."`
|
||||
}
|
||||
|
||||
// GlobalConfiguration holds global configuration (with providers, etc.).
|
||||
// It's populated from the traefik configuration file passed as an argument to the binary.
|
||||
type GlobalConfiguration struct {
|
||||
Port string
|
||||
GraceTimeOut int64
|
||||
AccessLogsFile string
|
||||
TraefikLogsFile string
|
||||
Certificates []Certificate
|
||||
LogLevel string
|
||||
ProvidersThrottleDuration time.Duration
|
||||
Docker *provider.Docker
|
||||
File *provider.File
|
||||
Web *WebProvider
|
||||
Marathon *provider.Marathon
|
||||
Consul *provider.Consul
|
||||
Etcd *provider.Etcd
|
||||
Zookeeper *provider.Zookepper
|
||||
Boltdb *provider.BoltDb
|
||||
GraceTimeOut int64 `short:"g" description:"Configuration file to use (TOML)."`
|
||||
Debug bool `short:"d" description:"Enable debug mode"`
|
||||
AccessLogsFile string `description:"Access logs file"`
|
||||
TraefikLogsFile string `description:"Traefik logs file"`
|
||||
LogLevel string `short:"l" description:"Log level"`
|
||||
EntryPoints EntryPoints `description:"Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'"`
|
||||
ACME *acme.ACME `description:"Enable ACME (Let's Encrypt): automatic SSL"`
|
||||
DefaultEntryPoints DefaultEntryPoints `description:"Entrypoints to be used by frontends that do not specify any entrypoint"`
|
||||
ProvidersThrottleDuration time.Duration `description:"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."`
|
||||
MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used"`
|
||||
Retry *Retry `description:"Enable retry sending request if network error"`
|
||||
Docker *provider.Docker `description:"Enable Docker backend"`
|
||||
File *provider.File `description:"Enable File backend"`
|
||||
Web *WebProvider `description:"Enable Web backend"`
|
||||
Marathon *provider.Marathon `description:"Enable Marathon backend"`
|
||||
Consul *provider.Consul `description:"Enable Consul backend"`
|
||||
ConsulCatalog *provider.ConsulCatalog `description:"Enable Consul catalog backend"`
|
||||
Etcd *provider.Etcd `description:"Enable Etcd backend"`
|
||||
Zookeeper *provider.Zookepper `description:"Enable Zookeeper backend"`
|
||||
Boltdb *provider.BoltDb `description:"Enable Boltdb backend"`
|
||||
Kubernetes *provider.Kubernetes `description:"Enable Kubernetes backend"`
|
||||
}
|
||||
|
||||
// DefaultEntryPoints holds default entry points
|
||||
type DefaultEntryPoints []string
|
||||
|
||||
// String is the method to format the flag's value, part of the flag.Value interface.
|
||||
// The String method's output will be used in diagnostics.
|
||||
func (dep *DefaultEntryPoints) String() string {
|
||||
return strings.Join(*dep, ",")
|
||||
}
|
||||
|
||||
// Set is the method to set the flag value, part of the flag.Value interface.
|
||||
// Set's argument is a string to be parsed to set the flag.
|
||||
// It's a comma-separated list, so we split it.
|
||||
func (dep *DefaultEntryPoints) Set(value string) error {
|
||||
entrypoints := strings.Split(value, ",")
|
||||
if len(entrypoints) == 0 {
|
||||
return errors.New("Bad DefaultEntryPoints format: " + value)
|
||||
}
|
||||
for _, entrypoint := range entrypoints {
|
||||
*dep = append(*dep, entrypoint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get return the EntryPoints map
|
||||
func (dep *DefaultEntryPoints) Get() interface{} { return DefaultEntryPoints(*dep) }
|
||||
|
||||
// SetValue sets the EntryPoints map with val
|
||||
func (dep *DefaultEntryPoints) SetValue(val interface{}) {
|
||||
*dep = DefaultEntryPoints(val.(DefaultEntryPoints))
|
||||
}
|
||||
|
||||
// Type is type of the struct
|
||||
func (dep *DefaultEntryPoints) Type() string {
|
||||
return fmt.Sprint("defaultentrypoints")
|
||||
}
|
||||
|
||||
// EntryPoints holds entry points configuration of the reverse proxy (ip, port, TLS...)
|
||||
type EntryPoints map[string]*EntryPoint
|
||||
|
||||
// String is the method to format the flag's value, part of the flag.Value interface.
|
||||
// The String method's output will be used in diagnostics.
|
||||
func (ep *EntryPoints) String() string {
|
||||
return fmt.Sprintf("%+v", *ep)
|
||||
}
|
||||
|
||||
// Set is the method to set the flag value, part of the flag.Value interface.
|
||||
// Set's argument is a string to be parsed to set the flag.
|
||||
// It's a comma-separated list, so we split it.
|
||||
func (ep *EntryPoints) Set(value string) error {
|
||||
regex := regexp.MustCompile("(?:Name:(?P<Name>\\S*))\\s*(?:Address:(?P<Address>\\S*))?\\s*(?:TLS:(?P<TLS>\\S*))?\\s*(?:Redirect.EntryPoint:(?P<RedirectEntryPoint>\\S*))?\\s*(?:Redirect.Regex:(?P<RedirectRegex>\\S*))?\\s*(?:Redirect.Replacement:(?P<RedirectReplacement>\\S*))?")
|
||||
match := regex.FindAllStringSubmatch(value, -1)
|
||||
if match == nil {
|
||||
return errors.New("Bad EntryPoints format: " + value)
|
||||
}
|
||||
matchResult := match[0]
|
||||
result := make(map[string]string)
|
||||
for i, name := range regex.SubexpNames() {
|
||||
if i != 0 {
|
||||
result[name] = matchResult[i]
|
||||
}
|
||||
}
|
||||
var tls *TLS
|
||||
if len(result["TLS"]) > 0 {
|
||||
certs := Certificates{}
|
||||
if err := certs.Set(result["TLS"]); err != nil {
|
||||
return err
|
||||
}
|
||||
tls = &TLS{
|
||||
Certificates: certs,
|
||||
}
|
||||
}
|
||||
var redirect *Redirect
|
||||
if len(result["RedirectEntryPoint"]) > 0 || len(result["RedirectRegex"]) > 0 || len(result["RedirectReplacement"]) > 0 {
|
||||
redirect = &Redirect{
|
||||
EntryPoint: result["RedirectEntryPoint"],
|
||||
Regex: result["RedirectRegex"],
|
||||
Replacement: result["RedirectReplacement"],
|
||||
}
|
||||
}
|
||||
|
||||
(*ep)[result["Name"]] = &EntryPoint{
|
||||
Address: result["Address"],
|
||||
TLS: tls,
|
||||
Redirect: redirect,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get return the EntryPoints map
|
||||
func (ep *EntryPoints) Get() interface{} { return EntryPoints(*ep) }
|
||||
|
||||
// SetValue sets the EntryPoints map with val
|
||||
func (ep *EntryPoints) SetValue(val interface{}) {
|
||||
*ep = EntryPoints(val.(EntryPoints))
|
||||
}
|
||||
|
||||
// Type is type of the struct
|
||||
func (ep *EntryPoints) Type() string {
|
||||
return fmt.Sprint("entrypoints²")
|
||||
}
|
||||
|
||||
// EntryPoint holds an entry point configuration of the reverse proxy (ip, port, TLS...)
|
||||
type EntryPoint struct {
|
||||
Network string
|
||||
Address string
|
||||
TLS *TLS
|
||||
Redirect *Redirect
|
||||
}
|
||||
|
||||
// Redirect configures a redirection of an entry point to another, or to an URL
|
||||
type Redirect struct {
|
||||
EntryPoint string
|
||||
Regex string
|
||||
Replacement string
|
||||
}
|
||||
|
||||
// TLS configures TLS for an entry point
|
||||
type TLS struct {
|
||||
Certificates Certificates
|
||||
}
|
||||
|
||||
// Certificates defines traefik certificates type
|
||||
type Certificates []Certificate
|
||||
|
||||
// String is the method to format the flag's value, part of the flag.Value interface.
|
||||
// The String method's output will be used in diagnostics.
|
||||
func (certs *Certificates) String() string {
|
||||
if len(*certs) == 0 {
|
||||
return ""
|
||||
}
|
||||
return (*certs)[0].CertFile + "," + (*certs)[0].KeyFile
|
||||
}
|
||||
|
||||
// Set is the method to set the flag value, part of the flag.Value interface.
|
||||
// Set's argument is a string to be parsed to set the flag.
|
||||
// It's a comma-separated list, so we split it.
|
||||
func (certs *Certificates) Set(value string) error {
|
||||
files := strings.Split(value, ",")
|
||||
if len(files) != 2 {
|
||||
return errors.New("Bad certificates format: " + value)
|
||||
}
|
||||
*certs = append(*certs, Certificate{
|
||||
CertFile: files[0],
|
||||
KeyFile: files[1],
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Type is type of the struct
|
||||
func (certs *Certificates) Type() string {
|
||||
return fmt.Sprint("certificates")
|
||||
}
|
||||
|
||||
// Certificate holds a SSL cert/key pair
|
||||
@@ -35,25 +203,103 @@ type Certificate struct {
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
// NewGlobalConfiguration returns a GlobalConfiguration with default values.
|
||||
func NewGlobalConfiguration() *GlobalConfiguration {
|
||||
globalConfiguration := new(GlobalConfiguration)
|
||||
// default values
|
||||
globalConfiguration.Port = ":80"
|
||||
globalConfiguration.GraceTimeOut = 10
|
||||
globalConfiguration.LogLevel = "ERROR"
|
||||
globalConfiguration.ProvidersThrottleDuration = time.Duration(2 * time.Second)
|
||||
|
||||
return globalConfiguration
|
||||
// Retry contains request retry config
|
||||
type Retry struct {
|
||||
Attempts int `description:"Number of attempts"`
|
||||
MaxMem int64 `description:"Maximum request body to be stored in memory in Mo"`
|
||||
}
|
||||
|
||||
// LoadFileConfig returns a GlobalConfiguration from reading the specified file (a toml file).
|
||||
func LoadFileConfig(file string) *GlobalConfiguration {
|
||||
configuration := NewGlobalConfiguration()
|
||||
if _, err := toml.DecodeFile(file, configuration); err != nil {
|
||||
fmtlog.Fatalf("Error reading file: %s", err)
|
||||
// NewTraefikDefaultPointersConfiguration creates a TraefikConfiguration with pointers default values
|
||||
func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
|
||||
//default Docker
|
||||
var defaultDocker provider.Docker
|
||||
defaultDocker.Watch = true
|
||||
defaultDocker.Endpoint = "unix:///var/run/docker.sock"
|
||||
defaultDocker.TLS = &provider.DockerTLS{}
|
||||
|
||||
// default File
|
||||
var defaultFile provider.File
|
||||
defaultFile.Watch = true
|
||||
defaultFile.Filename = "" //needs equivalent to viper.ConfigFileUsed()
|
||||
|
||||
// default Web
|
||||
var defaultWeb WebProvider
|
||||
defaultWeb.Address = ":8080"
|
||||
|
||||
// default Marathon
|
||||
var defaultMarathon provider.Marathon
|
||||
defaultMarathon.Watch = true
|
||||
defaultMarathon.Endpoint = "http://127.0.0.1:8080"
|
||||
defaultMarathon.ExposedByDefault = true
|
||||
|
||||
// default Consul
|
||||
var defaultConsul provider.Consul
|
||||
defaultConsul.Watch = true
|
||||
defaultConsul.Endpoint = "127.0.0.1:8500"
|
||||
defaultConsul.Prefix = "/traefik"
|
||||
defaultConsul.TLS = &provider.KvTLS{}
|
||||
|
||||
// default ConsulCatalog
|
||||
var defaultConsulCatalog provider.ConsulCatalog
|
||||
defaultConsulCatalog.Endpoint = "127.0.0.1:8500"
|
||||
|
||||
// default Etcd
|
||||
var defaultEtcd provider.Etcd
|
||||
defaultEtcd.Watch = true
|
||||
defaultEtcd.Endpoint = "127.0.0.1:400"
|
||||
defaultEtcd.Prefix = "/traefik"
|
||||
defaultEtcd.TLS = &provider.KvTLS{}
|
||||
|
||||
//default Zookeeper
|
||||
var defaultZookeeper provider.Zookepper
|
||||
defaultZookeeper.Watch = true
|
||||
defaultZookeeper.Endpoint = "127.0.0.1:2181"
|
||||
defaultZookeeper.Prefix = "/traefik"
|
||||
|
||||
//default Boltdb
|
||||
var defaultBoltDb provider.BoltDb
|
||||
defaultBoltDb.Watch = true
|
||||
defaultBoltDb.Endpoint = "127.0.0.1:4001"
|
||||
defaultBoltDb.Prefix = "/traefik"
|
||||
|
||||
//default Kubernetes
|
||||
var defaultKubernetes provider.Kubernetes
|
||||
defaultKubernetes.Watch = true
|
||||
defaultKubernetes.Endpoint = "127.0.0.1:8080"
|
||||
|
||||
defaultConfiguration := GlobalConfiguration{
|
||||
Docker: &defaultDocker,
|
||||
File: &defaultFile,
|
||||
Web: &defaultWeb,
|
||||
Marathon: &defaultMarathon,
|
||||
Consul: &defaultConsul,
|
||||
ConsulCatalog: &defaultConsulCatalog,
|
||||
Etcd: &defaultEtcd,
|
||||
Zookeeper: &defaultZookeeper,
|
||||
Boltdb: &defaultBoltDb,
|
||||
Kubernetes: &defaultKubernetes,
|
||||
Retry: &Retry{MaxMem: 2},
|
||||
}
|
||||
return &TraefikConfiguration{
|
||||
GlobalConfiguration: defaultConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
// NewTraefikConfiguration creates a TraefikConfiguration with default values
|
||||
func NewTraefikConfiguration() *TraefikConfiguration {
|
||||
return &TraefikConfiguration{
|
||||
GlobalConfiguration: GlobalConfiguration{
|
||||
GraceTimeOut: 10,
|
||||
AccessLogsFile: "",
|
||||
TraefikLogsFile: "",
|
||||
LogLevel: "ERROR",
|
||||
EntryPoints: map[string]*EntryPoint{},
|
||||
DefaultEntryPoints: []string{},
|
||||
ProvidersThrottleDuration: time.Duration(2 * time.Second),
|
||||
MaxIdleConnsPerHost: 200,
|
||||
},
|
||||
ConfigFile: "",
|
||||
}
|
||||
return configuration
|
||||
}
|
||||
|
||||
type configs map[string]*types.Configuration
|
||||
|
6
contrib/systemd/traefik.service
Normal file
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
Description=Traefik
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/traefik --configFile=/etc/traefik.toml
|
||||
Restart=on-failure
|
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 -t20 -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 -t20 -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/apollo-logo.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
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/asteris.logo.png
Normal file
After Width: | Height: | Size: 10 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 |
BIN
docs/img/mantl-logo.png
Normal file
After Width: | Height: | Size: 19 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 |
BIN
docs/img/zenika.logo.png
Normal file
After Width: | Height: | Size: 9.0 KiB |
970
docs/index.md
976
docs/toml.md
Normal file
@@ -0,0 +1,976 @@
|
||||
|
||||
# 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"
|
||||
# namespaces = ["default","production"]
|
||||
```
|
||||
|
||||
Annotations can be used on containers to override default behaviour for the whole Ingress resource:
|
||||
|
||||
- `traefik.frontend.rule.type: PathPrefixStrip`: override the default frontend rule (Default: `Host:{containerName}.{domain}`).
|
||||
|
||||
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` | `PathPrefix:/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']
|
12
examples/compose-k8s.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
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.23.0-1.0.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.23.0-1.0.ubuntu1404
|
||||
image: mesosphere/mesos-slave:0.28.1-2.0.20.ubuntu1404
|
||||
net: host
|
||||
pid: host
|
||||
privileged: true
|
||||
@@ -31,12 +31,22 @@ slave:
|
||||
- /usr/bin/docker:/usr/bin/docker:ro
|
||||
- /usr/lib/x86_64-linux-gnu/libapparmor.so.1:/usr/lib/x86_64-linux-gnu/libapparmor.so.1:ro
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /lib/x86_64-linux-gnu/libsystemd-journal.so.0:/lib/x86_64-linux-gnu/libsystemd-journal.so.0
|
||||
|
||||
marathon:
|
||||
image: mesosphere/marathon:v0.9.2
|
||||
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"
|
@@ -16,10 +16,10 @@ 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 "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 "http" http://localhost:8500/v1/kv/traefik/frontends/frontend1/entrypoints
|
||||
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 "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
|
111
examples/k8s.ingress.yaml
Normal file
@@ -0,0 +1,111 @@
|
||||
# 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
|
||||
|
||||
---
|
||||
# Another Ingress with PathPrefixStrip
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: whoami-ingress-stripped
|
||||
annotations:
|
||||
traefik.frontend.rule.type: "PathPrefixStrip"
|
||||
spec:
|
||||
rules:
|
||||
- host: foo.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /prefixWillBeStripped
|
||||
backend:
|
||||
serviceName: service1
|
||||
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
|
@@ -10,14 +10,7 @@
|
||||
"network": "BRIDGE",
|
||||
"portMappings": [
|
||||
{ "containerPort": 80, "hostPort": 0, "protocol": "tcp" }
|
||||
],
|
||||
"parameters": [{
|
||||
"key": "log-driver",
|
||||
"value": "gelf"
|
||||
}, {
|
||||
"key": "log-opt",
|
||||
"value": "gelf-address=udp://172.17.42.1:12201"
|
||||
}]
|
||||
]
|
||||
}
|
||||
},
|
||||
"healthChecks": [
|
||||
@@ -32,8 +25,7 @@
|
||||
],
|
||||
"labels": {
|
||||
"traefik.weight": "1",
|
||||
"traefik.protocole": "https",
|
||||
"traefik.frontend.rule": "Path",
|
||||
"traefik.frontend.value": "/test"
|
||||
"traefik.protocol": "http",
|
||||
"traefik.frontend.rule" : "Host:test.marathon.localhost"
|
||||
}
|
||||
}
|
@@ -3,6 +3,7 @@ Copyright
|
||||
*/
|
||||
|
||||
//go:generate rm -vf autogen/gen.go
|
||||
//go:generate mkdir -p static
|
||||
//go:generate go-bindata -pkg autogen -o autogen/gen.go ./static/... ./templates/...
|
||||
|
||||
//go:generate mkdir -p vendor/github.com/docker/docker/autogen/dockerversion
|
||||
|
216
glide.lock
generated
Normal file
@@ -0,0 +1,216 @@
|
||||
hash: 1d42530971835a5a1e593e8efc54f28f2714cb8d1dfcb333e3b9ba92ef1917cc
|
||||
updated: 2016-05-30T16:53:17.058435869+02:00
|
||||
imports:
|
||||
- name: github.com/boltdb/bolt
|
||||
version: dfb21201d9270c1082d5fb0f07f500311ff72f18
|
||||
- name: github.com/BurntSushi/toml
|
||||
version: f0aeabca5a127c4078abb8c8d64298b147264b55
|
||||
- name: github.com/BurntSushi/ty
|
||||
version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74
|
||||
subpackages:
|
||||
- fun
|
||||
- name: github.com/cenkalti/backoff
|
||||
version: c29158af31815ccc31ca29c86c121bc39e00d3d8
|
||||
- name: github.com/codahale/hdrhistogram
|
||||
version: 9208b142303c12d8899bae836fd524ac9338b4fd
|
||||
- name: github.com/codegangsta/cli
|
||||
version: bf4a526f48af7badd25d2cb02d587e1b01be3b50
|
||||
- name: github.com/codegangsta/negroni
|
||||
version: ffbc66b612ee3eac2eba29aedce4c3a65e4dd0a1
|
||||
- name: github.com/containous/flaeg
|
||||
version: a208db24c0e580be76efed167736fe3204c7c954
|
||||
- name: github.com/containous/oxy
|
||||
version: 183212964e13e7b8afe01a08b193d04300554a68
|
||||
subpackages:
|
||||
- cbreaker
|
||||
- connlimit
|
||||
- forward
|
||||
- roundrobin
|
||||
- stream
|
||||
- utils
|
||||
- name: github.com/containous/staert
|
||||
version: 9f815ef829b66de3519fea6c0b8d4708eb9472d4
|
||||
- name: github.com/coreos/etcd
|
||||
version: c400d05d0aa73e21e431c16145e558d624098018
|
||||
subpackages:
|
||||
- Godeps/_workspace/src/github.com/ugorji/go/codec
|
||||
- Godeps/_workspace/src/golang.org/x/net/context
|
||||
- client
|
||||
- pkg/pathutil
|
||||
- pkg/types
|
||||
- name: github.com/davecgh/go-spew
|
||||
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
|
||||
subpackages:
|
||||
- spew
|
||||
- name: github.com/docker/distribution
|
||||
version: 5bbf65499960b184fe8e0f045397375e1a6722b8
|
||||
subpackages:
|
||||
- reference
|
||||
- digest
|
||||
- name: github.com/docker/docker
|
||||
version: 9837ec4da53f15f9120d53a6e1517491ba8b0261
|
||||
subpackages:
|
||||
- namesgenerator
|
||||
- name: github.com/docker/engine-api
|
||||
version: 3d3d0b6c9d2651aac27f416a6da0224c1875b3eb
|
||||
subpackages:
|
||||
- client
|
||||
- types
|
||||
- types/events
|
||||
- types/filters
|
||||
- client/transport
|
||||
- client/transport/cancellable
|
||||
- types/container
|
||||
- types/network
|
||||
- types/reference
|
||||
- types/registry
|
||||
- types/time
|
||||
- types/versions
|
||||
- types/blkiodev
|
||||
- types/strslice
|
||||
- name: github.com/docker/go-connections
|
||||
version: c7838b258fbfa3fe88eecfb2a0e08ea0dbd6a646
|
||||
subpackages:
|
||||
- sockets
|
||||
- tlsconfig
|
||||
- nat
|
||||
- name: github.com/docker/go-units
|
||||
version: 5d2041e26a699eaca682e2ea41c8f891e1060444
|
||||
- name: github.com/docker/libcompose
|
||||
version: 8ee7bcc364f7b8194581a3c6bd9fa019467c7873
|
||||
- name: github.com/docker/libkv
|
||||
version: 7283ef27ed32fe267388510a91709b307bb9942c
|
||||
subpackages:
|
||||
- store
|
||||
- store/boltdb
|
||||
- store/consul
|
||||
- store/etcd
|
||||
- store/zookeeper
|
||||
- name: github.com/donovanhide/eventsource
|
||||
version: c3f57f280ec708df24886d9e62f2fd178d69d8e8
|
||||
- name: github.com/elazarl/go-bindata-assetfs
|
||||
version: 57eb5e1fc594ad4b0b1dbea7b286d299e0cb43c2
|
||||
- name: github.com/gambol99/go-marathon
|
||||
version: ade11d1dc2884ee1f387078fc28509559b6235d1
|
||||
- name: github.com/go-check/check
|
||||
version: 4f90aeace3a26ad7021961c297b22c42160c7b25
|
||||
- name: github.com/google/go-querystring
|
||||
version: 9235644dd9e52eeae6fa48efd539fdc351a0af53
|
||||
subpackages:
|
||||
- query
|
||||
- name: github.com/gorilla/context
|
||||
version: a8d44e7d8e4d532b6a27a02dd82abb31cc1b01bd
|
||||
- name: github.com/gorilla/mux
|
||||
version: 9c19ed558d5df4da88e2ade9c8940d742aef0e7e
|
||||
- name: github.com/hashicorp/consul
|
||||
version: f6fef66e1bf17be4f3c9855fbec6de802ca6bd7d
|
||||
subpackages:
|
||||
- api
|
||||
- name: github.com/hashicorp/go-cleanhttp
|
||||
version: 875fb671b3ddc66f8e2f0acc33829c8cb989a38d
|
||||
- name: github.com/hashicorp/serf
|
||||
version: e4ec8cc423bbe20d26584b96efbeb9102e16d05f
|
||||
subpackages:
|
||||
- coordinate
|
||||
- serf
|
||||
- name: github.com/libkermit/docker
|
||||
version: 3b5eb2973efff7af33cfb65141deaf4ed25c6d02
|
||||
- name: github.com/libkermit/docker-check
|
||||
version: bb75a86b169c6c5d22c0ee98278124036f272d7b
|
||||
- name: github.com/mailgun/log
|
||||
version: 2f35a4607f1abf71f97f77f99b0de8493ef6f4ef
|
||||
- name: github.com/mailgun/manners
|
||||
version: fada45142db3f93097ca917da107aa3fad0ffcb5
|
||||
- name: github.com/mailgun/multibuf
|
||||
version: 565402cd71fbd9c12aa7e295324ea357e970a61e
|
||||
- name: github.com/mailgun/timetools
|
||||
version: fd192d755b00c968d312d23f521eb0cdc6f66bd0
|
||||
- name: github.com/mattn/go-shellwords
|
||||
version: 525bedee691b5a8df547cb5cf9f86b7fb1883e24
|
||||
- name: github.com/Microsoft/go-winio
|
||||
version: 4f1a71750d95a5a8a46c40a67ffbed8129c2f138
|
||||
- name: github.com/miekg/dns
|
||||
version: 48ab6605c66ac797e07f615101c3e9e10e932b66
|
||||
- name: github.com/moul/http2curl
|
||||
version: b1479103caacaa39319f75e7f57fc545287fca0d
|
||||
- name: github.com/ogier/pflag
|
||||
version: 45c278ab3607870051a2ea9040bb85fcb8557481
|
||||
- name: github.com/opencontainers/runc
|
||||
version: d2d09b9bcd0573c58d7cd94e57bd7555af0c2072
|
||||
subpackages:
|
||||
- libcontainer/user
|
||||
- name: github.com/parnurzeal/gorequest
|
||||
version: 2169dfca686cfcbefc983a98a25e9c22a2815be4
|
||||
- name: github.com/pmezard/go-difflib
|
||||
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
|
||||
subpackages:
|
||||
- difflib
|
||||
- name: github.com/samuel/go-zookeeper
|
||||
version: 6eb1b09c6ce23f305f4c81bf748b22fbc6f3f9e9
|
||||
subpackages:
|
||||
- zk
|
||||
- name: github.com/Sirupsen/logrus
|
||||
version: 6d9ae300aaf85d6acd2e5424081c7fcddb21dab8
|
||||
- name: github.com/streamrail/concurrent-map
|
||||
version: 65a174a3a4188c0b7099acbc6cfa0c53628d3287
|
||||
- name: github.com/stretchr/objx
|
||||
version: cbeaeb16a013161a98496fad62933b1d21786672
|
||||
- name: github.com/stretchr/testify
|
||||
version: 6cb3b85ef5a0efef77caef88363ec4d4b5c0976d
|
||||
subpackages:
|
||||
- mock
|
||||
- assert
|
||||
- name: github.com/thoas/stats
|
||||
version: 69e3c072eec2df2df41afe6214f62eb940e4cd80
|
||||
- name: github.com/unrolled/render
|
||||
version: 198ad4d8b8a4612176b804ca10555b222a086b40
|
||||
- name: github.com/vdemeester/docker-events
|
||||
version: b308d2e8d639d928c882913bcb4f85b3a84c7a07
|
||||
- name: github.com/vdemeester/shakers
|
||||
version: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3
|
||||
- name: github.com/vulcand/oxy
|
||||
version: 11677428db34c4a05354d66d028174d0e3c6e905
|
||||
subpackages:
|
||||
- memmetrics
|
||||
- utils
|
||||
- name: github.com/vulcand/predicate
|
||||
version: cb0bff91a7ab7cf7571e661ff883fc997bc554a3
|
||||
- name: github.com/vulcand/route
|
||||
version: cb89d787ddbb1c5849a7ac9f79004c1fd12a4a32
|
||||
- name: github.com/vulcand/vulcand
|
||||
version: 475540bb016702d5b7cc4674e37f48ee3e144a69
|
||||
subpackages:
|
||||
- plugin/rewrite
|
||||
- plugin
|
||||
- router
|
||||
- name: github.com/xenolf/lego
|
||||
version: b119bc45fbd1cc71348003541aac9d3a7da63654
|
||||
subpackages:
|
||||
- acme
|
||||
- name: golang.org/x/crypto
|
||||
version: 5bcd134fee4dd1475da17714aac19c0aa0142e2f
|
||||
subpackages:
|
||||
- ocsp
|
||||
- name: golang.org/x/net
|
||||
version: 6460565bec1e8891e29ff478184c71b9e443ac36
|
||||
subpackages:
|
||||
- context
|
||||
- publicsuffix
|
||||
- proxy
|
||||
- name: golang.org/x/sys
|
||||
version: eb2c74142fd19a79b3f237334c7384d5167b1b46
|
||||
subpackages:
|
||||
- unix
|
||||
- windows
|
||||
- name: gopkg.in/fsnotify.v1
|
||||
version: 30411dbcefb7a1da7e84f75530ad3abe4011b4f8
|
||||
- name: gopkg.in/mgo.v2
|
||||
version: b6e2fa371e64216a45e61072a96d4e3859f169da
|
||||
subpackages:
|
||||
- bson
|
||||
- name: gopkg.in/square/go-jose.v1
|
||||
version: e3f973b66b91445ec816dd7411ad1b6495a5a2fc
|
||||
subpackages:
|
||||
- cipher
|
||||
- json
|
||||
devImports: []
|
225
glide.yaml
@@ -1,149 +1,78 @@
|
||||
package: main
|
||||
package: github.com/containous/traefik
|
||||
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/mailgun/oxy
|
||||
ref: 547c334d658398c05b346c0b79d8f47ba2e1473b
|
||||
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: 8ce3f764250b2de3f2c627d12ca7dd21bd5e7f93
|
||||
- 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: 0239034d42f665efa17fd77c39f891c2f9f32922
|
||||
- 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: 79ef5d150f053a5b12f16b02d8844ed7cf33611a
|
||||
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: 37136f736785d7c6aa3b9a27b4b2dd1028ca6d79
|
||||
- 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/BurntSushi/toml
|
||||
- package: github.com/BurntSushi/ty
|
||||
subpackages:
|
||||
- fun
|
||||
- package: github.com/Sirupsen/logrus
|
||||
- package: github.com/cenkalti/backoff
|
||||
- package: github.com/codegangsta/negroni
|
||||
- package: github.com/containous/flaeg
|
||||
version: a208db24c0e580be76efed167736fe3204c7c954
|
||||
- package: github.com/containous/oxy
|
||||
subpackages:
|
||||
- cbreaker
|
||||
- connlimit
|
||||
- forward
|
||||
- roundrobin
|
||||
- stream
|
||||
- utils
|
||||
- package: github.com/containous/staert
|
||||
version: 9f815ef829b66de3519fea6c0b8d4708eb9472d4
|
||||
- package: github.com/docker/engine-api
|
||||
version: 3d3d0b6c9d2651aac27f416a6da0224c1875b3eb
|
||||
subpackages:
|
||||
- client
|
||||
- types
|
||||
- types/events
|
||||
- types/filters
|
||||
- package: github.com/docker/go-connections
|
||||
subpackages:
|
||||
- sockets
|
||||
- tlsconfig
|
||||
- package: github.com/docker/libkv
|
||||
subpackages:
|
||||
- store
|
||||
- store/boltdb
|
||||
- store/consul
|
||||
- store/etcd
|
||||
- store/zookeeper
|
||||
- package: github.com/elazarl/go-bindata-assetfs
|
||||
- package: github.com/gambol99/go-marathon
|
||||
version: ade11d1dc2884ee1f387078fc28509559b6235d1
|
||||
- package: github.com/gorilla/mux
|
||||
- package: github.com/hashicorp/consul
|
||||
subpackages:
|
||||
- api
|
||||
- package: github.com/mailgun/manners
|
||||
- package: github.com/parnurzeal/gorequest
|
||||
- package: github.com/streamrail/concurrent-map
|
||||
- package: github.com/stretchr/testify
|
||||
subpackages:
|
||||
- mock
|
||||
- package: github.com/thoas/stats
|
||||
- package: github.com/unrolled/render
|
||||
- package: github.com/vdemeester/docker-events
|
||||
- package: github.com/vulcand/vulcand
|
||||
subpackages:
|
||||
- plugin/rewrite
|
||||
- package: github.com/xenolf/lego
|
||||
subpackages:
|
||||
- acme
|
||||
- package: golang.org/x/net
|
||||
subpackages:
|
||||
- context
|
||||
- package: gopkg.in/fsnotify.v1
|
||||
- package: github.com/libkermit/docker-check
|
||||
version: bb75a86b169c6c5d22c0ee98278124036f272d7b
|
||||
- package: github.com/libkermit/docker
|
||||
version: 3b5eb2973efff7af33cfb65141deaf4ed25c6d02
|
||||
- package: github.com/docker/docker
|
||||
version: 9837ec4da53f15f9120d53a6e1517491ba8b0261
|
||||
subpackages:
|
||||
- namesgenerator
|
||||
- package: github.com/go-check/check
|
||||
- package: github.com/docker/libcompose
|
||||
version: 8ee7bcc364f7b8194581a3c6bd9fa019467c7873
|
||||
- package: github.com/mattn/go-shellwords
|
||||
- package: github.com/vdemeester/shakers
|
||||
|
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
|
||||
}
|
@@ -1,58 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
"bytes"
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// SimpleSuite
|
||||
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 traefik.toml: no such file or directory")
|
||||
|
||||
nonExistentFile := "non/existent/file.toml"
|
||||
cmd = exec.Command(traefikBinary, 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))
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestInvalidConfigShouldFail(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "fixtures/invalid_configuration.toml")
|
||||
output, err := cmd.CombinedOutput()
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/invalid_configuration.toml")
|
||||
|
||||
c.Assert(err, checker.NotNil)
|
||||
c.Assert(string(output), checker.Contains, "Error reading file: 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, "Near line 0 (last key parsed ''): Bare keys cannot contain '{'")
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestSimpleDefaultConfig(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "fixtures/simple_default.toml")
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/simple_default.toml")
|
||||
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/")
|
||||
resp, err := http.Get("http://127.0.0.1:8000/")
|
||||
|
||||
// Expected a 404 as we did not comfigure anything
|
||||
// Expected a 404 as we did not configure anything
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 404)
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestWithWebConfig(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "fixtures/simple_web.toml")
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/simple_web.toml")
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
113
integration/consul_catalog_test.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
"github.com/hashicorp/consul/api"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
)
|
||||
|
||||
// Consul catalog test suites
|
||||
type ConsulCatalogSuite struct {
|
||||
BaseSuite
|
||||
consulIP string
|
||||
consulClient *api.Client
|
||||
}
|
||||
|
||||
func (s *ConsulCatalogSuite) SetUpSuite(c *check.C) {
|
||||
|
||||
s.createComposeProject(c, "consul_catalog")
|
||||
s.composeProject.Start(c)
|
||||
|
||||
consul := s.composeProject.Container(c, "consul")
|
||||
|
||||
s.consulIP = consul.NetworkSettings.IPAddress
|
||||
config := api.DefaultConfig()
|
||||
config.Address = s.consulIP + ":8500"
|
||||
consulClient, err := api.NewClient(config)
|
||||
if err != nil {
|
||||
c.Fatalf("Error creating consul client")
|
||||
}
|
||||
s.consulClient = consulClient
|
||||
|
||||
// Wait for consul to elect itself leader
|
||||
time.Sleep(2000 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (s *ConsulCatalogSuite) registerService(name string, address string, port int, tags []string) error {
|
||||
catalog := s.consulClient.Catalog()
|
||||
_, err := catalog.Register(
|
||||
&api.CatalogRegistration{
|
||||
Node: address,
|
||||
Address: address,
|
||||
Service: &api.AgentService{
|
||||
ID: name,
|
||||
Service: name,
|
||||
Address: address,
|
||||
Port: port,
|
||||
Tags: tags,
|
||||
},
|
||||
},
|
||||
&api.WriteOptions{},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ConsulCatalogSuite) deregisterService(name string, address string) error {
|
||||
catalog := s.consulClient.Catalog()
|
||||
_, err := catalog.Deregister(
|
||||
&api.CatalogDeregistration{
|
||||
Node: address,
|
||||
Address: address,
|
||||
ServiceID: name,
|
||||
},
|
||||
&api.WriteOptions{},
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *ConsulCatalogSuite) TestSimpleConfiguration(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "--consulCatalog", "--consulCatalog.endpoint="+s.consulIP+":8500", "--configFile=fixtures/consul_catalog/simple.toml")
|
||||
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 *ConsulCatalogSuite) TestSingleService(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "--consulCatalog", "--consulCatalog.endpoint="+s.consulIP+":8500", "--consulCatalog.domain=consul.localhost", "--configFile=fixtures/consul_catalog/simple.toml")
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
nginx := s.composeProject.Container(c, "nginx")
|
||||
|
||||
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)
|
||||
|
||||
time.Sleep(5000 * time.Millisecond)
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
req.Host = "test.consul.localhost"
|
||||
resp, err := client.Do(req)
|
||||
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 200)
|
||||
|
||||
_, err = ioutil.ReadAll(resp.Body)
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
@@ -5,21 +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
|
||||
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, "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/")
|
||||
resp, err := http.Get("http://127.0.0.1:8000/")
|
||||
|
||||
// Expected a 404 as we did not comfigure anything
|
||||
// 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)
|
||||
}
|
||||
|
@@ -7,13 +7,15 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/docker/opts"
|
||||
"github.com/docker/docker/pkg/namesgenerator"
|
||||
"github.com/fsouza/go-dockerclient"
|
||||
"github.com/go-check/check"
|
||||
|
||||
d "github.com/libkermit/docker"
|
||||
docker "github.com/libkermit/docker-check"
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -31,122 +33,60 @@ var (
|
||||
// Docker test suites
|
||||
type DockerSuite struct {
|
||||
BaseSuite
|
||||
client *docker.Client
|
||||
project *docker.Project
|
||||
}
|
||||
|
||||
func (s *DockerSuite) startContainer(c *check.C, image string, args ...string) string {
|
||||
return s.startContainerWithConfig(c, docker.CreateContainerOptions{
|
||||
Config: &docker.Config{
|
||||
Image: image,
|
||||
Cmd: args,
|
||||
},
|
||||
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, docker.CreateContainerOptions{
|
||||
Config: &docker.Config{
|
||||
Image: image,
|
||||
Cmd: args,
|
||||
Labels: labels,
|
||||
},
|
||||
return s.startContainerWithConfig(c, image, d.ContainerConfig{
|
||||
Cmd: args,
|
||||
Labels: labels,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *DockerSuite) startContainerWithConfig(c *check.C, config docker.CreateContainerOptions) string {
|
||||
func (s *DockerSuite) startContainerWithConfig(c *check.C, image string, config d.ContainerConfig) string {
|
||||
if config.Name == "" {
|
||||
config.Name = namesgenerator.GetRandomName(10)
|
||||
}
|
||||
if config.Config.Labels == nil {
|
||||
config.Config.Labels = map[string]string{}
|
||||
}
|
||||
config.Config.Labels[TestLabel] = "true"
|
||||
|
||||
container, err := s.client.CreateContainer(config)
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error creating a container using config %v", config))
|
||||
container := s.project.StartWithConfig(c, image, config)
|
||||
|
||||
err = s.client.StartContainer(container.ID, &docker.HostConfig{})
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error starting container %v", container))
|
||||
|
||||
return container.Name
|
||||
// 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) {
|
||||
dockerHost := os.Getenv("DOCKER_HOST")
|
||||
if dockerHost == "" {
|
||||
// FIXME Handle windows -- see if dockerClient already handle that or not
|
||||
dockerHost = fmt.Sprintf("unix://%s", opts.DefaultUnixSocket)
|
||||
}
|
||||
// Make sure we can speak to docker
|
||||
dockerClient, err := docker.NewClient(dockerHost)
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error connecting to docker daemon"))
|
||||
|
||||
s.client = dockerClient
|
||||
c.Assert(s.client.Ping(), checker.IsNil)
|
||||
project := docker.NewProjectFromEnv(c)
|
||||
s.project = project
|
||||
|
||||
// Pull required images
|
||||
for repository, tag := range RequiredImages {
|
||||
image := fmt.Sprintf("%s:%s", repository, tag)
|
||||
_, err := s.client.InspectImage(image)
|
||||
if err != nil {
|
||||
if err != docker.ErrNoSuchImage {
|
||||
c.Fatalf("Error while inspect image %s", image)
|
||||
}
|
||||
err = s.client.PullImage(docker.PullImageOptions{
|
||||
Repository: repository,
|
||||
Tag: tag,
|
||||
}, docker.AuthConfiguration{})
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error while pulling image %s", image))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DockerSuite) cleanContainers(c *check.C) {
|
||||
// Clean the mess, a.k.a. the running containers with the right label
|
||||
containerList, err := s.client.ListContainers(docker.ListContainersOptions{
|
||||
Filters: map[string][]string{
|
||||
"label": {fmt.Sprintf("%s=true", TestLabel)},
|
||||
},
|
||||
})
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error listing containers started by traefik"))
|
||||
|
||||
for _, container := range containerList {
|
||||
err = s.client.KillContainer(docker.KillContainerOptions{
|
||||
ID: container.ID,
|
||||
})
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error killing container %v", container))
|
||||
if os.Getenv("CIRCLECI") == "" {
|
||||
// On circleci, we won't delete them — it errors out for now >_<
|
||||
err = s.client.RemoveContainer(docker.RemoveContainerOptions{
|
||||
ID: container.ID,
|
||||
RemoveVolumes: true,
|
||||
})
|
||||
c.Assert(err, checker.IsNil, check.Commentf("Error removing container %v", container))
|
||||
}
|
||||
s.project.Pull(c, image)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DockerSuite) TearDownTest(c *check.C) {
|
||||
s.cleanContainers(c)
|
||||
}
|
||||
|
||||
func (s *DockerSuite) TearDownSuite(c *check.C) {
|
||||
// Call cleanContainers, just in case (?)
|
||||
// s.cleanContainers(c)
|
||||
s.project.Clean(c, os.Getenv("CIRCLECI") != "")
|
||||
}
|
||||
|
||||
func (s *DockerSuite) TestSimpleConfiguration(c *check.C) {
|
||||
file := s.adaptFileForHost(c, "fixtures/docker/simple.toml")
|
||||
defer os.Remove(file)
|
||||
|
||||
cmd := exec.Command(traefikBinary, 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/")
|
||||
resp, err := http.Get("http://127.0.0.1:8000/")
|
||||
|
||||
c.Assert(err, checker.IsNil)
|
||||
// Expected a 404 as we did not comfigure anything
|
||||
@@ -159,7 +99,7 @@ func (s *DockerSuite) TestDefaultDockerContainers(c *check.C) {
|
||||
name := s.startContainer(c, "swarm:1.0.0", "manage", "token://blablabla")
|
||||
|
||||
// Start traefik
|
||||
cmd := exec.Command(traefikBinary, file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
@@ -168,7 +108,7 @@ func (s *DockerSuite) TestDefaultDockerContainers(c *check.C) {
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1/version", nil)
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/version", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
req.Host = fmt.Sprintf("%s.docker.localhost", name)
|
||||
resp, err := client.Do(req)
|
||||
@@ -190,13 +130,12 @@ 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")
|
||||
|
||||
// Start traefik
|
||||
cmd := exec.Command(traefikBinary, file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
@@ -205,7 +144,7 @@ func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) {
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1/version", nil)
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/version", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
req.Host = fmt.Sprintf("my.super.host")
|
||||
resp, err := client.Do(req)
|
||||
@@ -232,7 +171,7 @@ func (s *DockerSuite) TestDockerContainersWithOneMissingLabels(c *check.C) {
|
||||
s.startContainerWithLabels(c, "swarm:1.0.0", labels, "manage", "token://blabla")
|
||||
|
||||
// Start traefik
|
||||
cmd := exec.Command(traefikBinary, file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
@@ -241,7 +180,7 @@ func (s *DockerSuite) TestDockerContainersWithOneMissingLabels(c *check.C) {
|
||||
time.Sleep(1500 * time.Millisecond)
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1/version", nil)
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/version", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
req.Host = fmt.Sprintf("my.super.host")
|
||||
resp, err := client.Do(req)
|
||||
|
194
integration/etcd_test.go
Normal file
@@ -0,0 +1,194 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
|
||||
"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
|
||||
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) {
|
||||
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()
|
||||
|
||||
time.Sleep(1000 * 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 *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,18 +5,28 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// File test suites
|
||||
type FileSuite struct{ BaseSuite }
|
||||
|
||||
func (s *FileSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "file")
|
||||
|
||||
s.composeProject.Start(c)
|
||||
}
|
||||
|
||||
func (s *FileSuite) TestSimpleConfiguration(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "fixtures/file/simple.toml")
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/file/simple.toml")
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
resp, err := http.Get("http://127.0.0.1/")
|
||||
resp, err := http.Get("http://127.0.0.1:8000/")
|
||||
|
||||
// Expected a 404 as we did not configure anything
|
||||
c.Assert(err, checker.IsNil)
|
||||
@@ -25,13 +35,13 @@ func (s *FileSuite) TestSimpleConfiguration(c *check.C) {
|
||||
|
||||
// #56 regression test, make sure it does not fail
|
||||
func (s *FileSuite) TestSimpleConfigurationNoPanic(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "fixtures/file/56-simple-panic.toml")
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/file/56-simple-panic.toml")
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
resp, err := http.Get("http://127.0.0.1/")
|
||||
resp, err := http.Get("http://127.0.0.1:8000/")
|
||||
|
||||
// Expected a 404 as we did not configure anything
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
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,11 +1,16 @@
|
||||
# Reverse proxy port
|
||||
#
|
||||
# Optional
|
||||
# Default: ":80"
|
||||
#
|
||||
# port = ":80"
|
||||
#
|
||||
# LogLevel
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
|
||||
[consul]
|
||||
endpoint = "{{.ConsulHost}}:8500"
|
||||
watch = true
|
||||
prefix = "traefik"
|
||||
|
||||
[web]
|
||||
address = ":8081"
|
9
integration/fixtures/consul_catalog/simple.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
defaultEntryPoints = ["http"]
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
[consulCatalog]
|
||||
domain = "consul.localhost"
|
@@ -1,13 +1,11 @@
|
||||
# Reverse proxy port
|
||||
#
|
||||
# Optional
|
||||
# Default: ":80"
|
||||
#
|
||||
# port = ":80"
|
||||
#
|
||||
# LogLevel
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
[docker]
|
||||
|
||||
# It's dynamagic !
|
||||
|
16
integration/fixtures/etcd/simple.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
|
||||
[etcd]
|
||||
endpoint = "{{.EtcdHost}}:4001"
|
||||
prefix = "/traefik"
|
||||
watch = true
|
||||
|
||||
[web]
|
||||
address = ":8081"
|
@@ -1,11 +1,9 @@
|
||||
# Reverse proxy port
|
||||
#
|
||||
# Optional
|
||||
# Default: ":80"
|
||||
#
|
||||
# port = ":80"
|
||||
#
|
||||
# LogLevel
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[file]
|
||||
|
@@ -1,11 +1,9 @@
|
||||
# Reverse proxy port
|
||||
#
|
||||
# Optional
|
||||
# Default: ":80"
|
||||
#
|
||||
# port = ":80"
|
||||
#
|
||||
# LogLevel
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[file]
|
||||
@@ -35,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"
|
||||
|
@@ -1,13 +1,17 @@
|
||||
port = ":443"
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[[certificates]]
|
||||
CertFile = "fixtures/https/snitest.com.cert"
|
||||
KeyFile = "fixtures/https/snitest.com.key"
|
||||
defaultEntryPoints = ["https"]
|
||||
|
||||
[[certificates]]
|
||||
CertFile = "fixtures/https/snitest.org.cert"
|
||||
KeyFile = "fixtures/https/snitest.org.key"
|
||||
[entryPoints]
|
||||
[entryPoints.https]
|
||||
address = ":4443"
|
||||
[entryPoints.https.tls]
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
CertFile = "fixtures/https/snitest.com.cert"
|
||||
KeyFile = "fixtures/https/snitest.com.key"
|
||||
[[entryPoints.https.tls.certificates]]
|
||||
CertFile = "fixtures/https/snitest.org.cert"
|
||||
KeyFile = "fixtures/https/snitest.org.key"
|
||||
|
||||
[file]
|
||||
|
||||
@@ -23,10 +27,8 @@ KeyFile = "fixtures/https/snitest.org.key"
|
||||
[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"
|
||||
|
@@ -1,11 +1,9 @@
|
||||
# Reverse proxy port
|
||||
#
|
||||
# Optional
|
||||
# Default: ":80"
|
||||
#
|
||||
# port = ":80"
|
||||
#
|
||||
# LogLevel
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[marathon]
|
||||
|
@@ -1,9 +1,5 @@
|
||||
# Reverse proxy port
|
||||
#
|
||||
# Optional
|
||||
# Default: ":80"
|
||||
#
|
||||
# port = ":80"
|
||||
#
|
||||
# LogLevel
|
||||
logLevel = "DEBUG"
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
@@ -1,5 +1,9 @@
|
||||
logLevel = "DEBUG"
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
[web]
|
||||
|
||||
address = ":8080"
|
||||
|
@@ -8,8 +8,9 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// HTTPSSuite
|
||||
@@ -19,7 +20,7 @@ type HTTPSSuite struct{ BaseSuite }
|
||||
// "snitest.com", which happens to match the CN of 'snitest.com.crt'. The test
|
||||
// verifies that traefik presents the correct certificate.
|
||||
func (s *HTTPSSuite) TestWithSNIConfigHandshake(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "fixtures/https/https_sni.toml")
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/https/https_sni.toml")
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
@@ -30,7 +31,7 @@ func (s *HTTPSSuite) TestWithSNIConfigHandshake(c *check.C) {
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "snitest.com",
|
||||
}
|
||||
conn, err := tls.Dial("tcp", "127.0.0.1:443", tlsConfig)
|
||||
conn, err := tls.Dial("tcp", "127.0.0.1:4443", tlsConfig)
|
||||
c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server"))
|
||||
|
||||
defer conn.Close()
|
||||
@@ -46,7 +47,7 @@ func (s *HTTPSSuite) TestWithSNIConfigHandshake(c *check.C) {
|
||||
// SNI hostnames of "snitest.org" and "snitest.com". The test verifies
|
||||
// that traefik routes the requests to the expected backends.
|
||||
func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "fixtures/https/https_sni.toml")
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/https/https_sni.toml")
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
@@ -72,7 +73,7 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) {
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: tr1}
|
||||
req, _ := http.NewRequest("GET", "https://127.0.0.1/", nil)
|
||||
req, _ := http.NewRequest("GET", "https://127.0.0.1:4443/", nil)
|
||||
req.Host = "snitest.com"
|
||||
req.Header.Set("Host", "snitest.com")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
@@ -82,7 +83,7 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) {
|
||||
c.Assert(resp.StatusCode, checker.Equals, 204)
|
||||
|
||||
client = &http.Client{Transport: tr2}
|
||||
req, _ = http.NewRequest("GET", "https://127.0.0.1/", nil)
|
||||
req, _ = http.NewRequest("GET", "https://127.0.0.1:4443/", nil)
|
||||
req.Host = "snitest.org"
|
||||
req.Header.Set("Host", "snitest.org")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
|
@@ -10,12 +10,11 @@ import (
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/docker/libcompose/docker"
|
||||
"github.com/docker/libcompose/project"
|
||||
"github.com/emilevauge/traefik/integration/utils"
|
||||
"github.com/containous/traefik/integration/utils"
|
||||
"github.com/go-check/check"
|
||||
|
||||
"github.com/libkermit/docker-check/compose"
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
@@ -24,98 +23,33 @@ func Test(t *testing.T) {
|
||||
|
||||
func init() {
|
||||
check.Suite(&SimpleSuite{})
|
||||
check.Suite(&AccessLogSuite{})
|
||||
check.Suite(&HTTPSSuite{})
|
||||
check.Suite(&FileSuite{})
|
||||
check.Suite(&DockerSuite{})
|
||||
check.Suite(&ConsulSuite{})
|
||||
check.Suite(&ConsulCatalogSuite{})
|
||||
check.Suite(&EtcdSuite{})
|
||||
check.Suite(&MarathonSuite{})
|
||||
}
|
||||
|
||||
var traefikBinary = "../dist/traefik"
|
||||
|
||||
// File test suites
|
||||
type FileSuite struct{ BaseSuite }
|
||||
|
||||
func (s *FileSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "file")
|
||||
|
||||
s.composeProject.Up()
|
||||
}
|
||||
|
||||
// Consul test suites (using libcompose)
|
||||
type ConsulSuite struct{ BaseSuite }
|
||||
|
||||
func (s *ConsulSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "consul")
|
||||
}
|
||||
|
||||
// Marathon test suites (using libcompose)
|
||||
type MarathonSuite struct{ BaseSuite }
|
||||
|
||||
func (s *MarathonSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "marathon")
|
||||
}
|
||||
|
||||
type BaseSuite struct {
|
||||
composeProject *project.Project
|
||||
listenChan chan project.Event
|
||||
started chan bool
|
||||
stopped chan bool
|
||||
deleted chan bool
|
||||
composeProject *compose.Project
|
||||
}
|
||||
|
||||
func (s *BaseSuite) TearDownSuite(c *check.C) {
|
||||
// shutdown and delete compose project
|
||||
if s.composeProject != nil {
|
||||
s.composeProject.Down()
|
||||
<-s.stopped
|
||||
defer close(s.stopped)
|
||||
|
||||
s.composeProject.Delete()
|
||||
<-s.deleted
|
||||
defer close(s.deleted)
|
||||
s.composeProject.Stop(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BaseSuite) createComposeProject(c *check.C, name string) {
|
||||
composeProject, err := docker.NewProject(&docker.Context{
|
||||
Context: project.Context{
|
||||
ComposeFile: fmt.Sprintf("resources/compose/%s.yml", name),
|
||||
ProjectName: fmt.Sprintf("integration-test-%s", name),
|
||||
},
|
||||
})
|
||||
c.Assert(err, checker.IsNil)
|
||||
s.composeProject = composeProject
|
||||
|
||||
s.started = make(chan bool)
|
||||
s.stopped = make(chan bool)
|
||||
s.deleted = make(chan bool)
|
||||
|
||||
s.listenChan = make(chan project.Event)
|
||||
go s.startListening(c)
|
||||
|
||||
composeProject.AddListener(s.listenChan)
|
||||
|
||||
composeProject.Start()
|
||||
|
||||
// Wait for compose to start
|
||||
<-s.started
|
||||
defer close(s.started)
|
||||
}
|
||||
|
||||
func (s *BaseSuite) startListening(c *check.C) {
|
||||
for event := range s.listenChan {
|
||||
// FIXME Add a timeout on event ?
|
||||
if event.EventType == project.EventProjectStartDone {
|
||||
s.started <- true
|
||||
}
|
||||
if event.EventType == project.EventProjectDownDone {
|
||||
s.stopped <- true
|
||||
}
|
||||
if event.EventType == project.EventProjectDeleteDone {
|
||||
s.deleted <- true
|
||||
}
|
||||
}
|
||||
projectName := fmt.Sprintf("integration-test-%s", name)
|
||||
composeFile := fmt.Sprintf("resources/compose/%s.yml", name)
|
||||
s.composeProject = compose.CreateProject(c, projectName, composeFile)
|
||||
}
|
||||
|
||||
func (s *BaseSuite) traefikCmd(c *check.C, args ...string) (*exec.Cmd, string) {
|
||||
@@ -130,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)
|
||||
@@ -140,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,19 +5,27 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Marathon test suites (using libcompose)
|
||||
type MarathonSuite struct{ BaseSuite }
|
||||
|
||||
func (s *MarathonSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "marathon")
|
||||
}
|
||||
|
||||
func (s *MarathonSuite) TestSimpleConfiguration(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "fixtures/marathon/simple.toml")
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/marathon/simple.toml")
|
||||
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/")
|
||||
resp, err := http.Get("http://127.0.0.1:8000/")
|
||||
|
||||
// Expected a 404 as we did not configure anything
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
@@ -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
|
17
integration/resources/compose/consul_catalog.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
consul:
|
||||
image: progrium/consul
|
||||
command: -server -bootstrap -log-level debug -ui-dir /ui
|
||||
ports:
|
||||
- "8400:8400"
|
||||
- "8500:8500"
|
||||
- "8600:53/udp"
|
||||
expose:
|
||||
- "8300"
|
||||
- "8301"
|
||||
- "8301/udp"
|
||||
- "8302"
|
||||
- "8302/udp"
|
||||
nginx:
|
||||
image: nginx
|
||||
ports:
|
||||
- "8881:80"
|
14
integration/resources/compose/etcd.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
etcd:
|
||||
image: containous/docker-etcd
|
||||
|
||||
whoami1:
|
||||
image: emilevauge/whoami
|
||||
|
||||
whoami2:
|
||||
image: emilevauge/whoami
|
||||
|
||||
whoami3:
|
||||
image: emilevauge/whoami
|
||||
|
||||
whoami4:
|
||||
image: emilevauge/whoami
|
@@ -3,7 +3,7 @@ zk:
|
||||
net: host
|
||||
environment:
|
||||
ZK_CONFIG: tickTime=2000,initLimit=10,syncLimit=5,maxClientCnxns=128,forceSync=no,clientPort=2181
|
||||
ZK_ID: 1
|
||||
ZK_ID: " 1"
|
||||
|
||||
master:
|
||||
image: mesosphere/mesos-master:0.23.0-1.0.ubuntu1404
|
||||
@@ -12,7 +12,7 @@ master:
|
||||
MESOS_ZK: zk://127.0.0.1:2181/mesos
|
||||
MESOS_HOSTNAME: 127.0.0.1
|
||||
MESOS_IP: 127.0.0.1
|
||||
MESOS_QUORUM: 1
|
||||
MESOS_QUORUM: " 1"
|
||||
MESOS_CLUSTER: docker-compose
|
||||
MESOS_WORK_DIR: /var/lib/mesos
|
||||
|
||||
|
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
|
||||
}
|
||||
}
|
@@ -3,7 +3,7 @@ package middlewares
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mailgun/oxy/cbreaker"
|
||||
"github.com/containous/oxy/cbreaker"
|
||||
)
|
||||
|
||||
// CircuitBreaker holds the oxy circuit breaker.
|
||||
|
35
middlewares/handlerSwitcher.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HandlerSwitcher allows hot switching of http.ServeMux
|
||||
type HandlerSwitcher struct {
|
||||
handler *safe.Safe
|
||||
}
|
||||
|
||||
// NewHandlerSwitcher builds a new instance of HandlerSwitcher
|
||||
func NewHandlerSwitcher(newHandler *mux.Router) (hs *HandlerSwitcher) {
|
||||
return &HandlerSwitcher{
|
||||
handler: safe.New(newHandler),
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *HandlerSwitcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
handlerBackup := hs.handler.Get().(*mux.Router)
|
||||
handlerBackup.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
// GetHandler returns the current http.ServeMux
|
||||
func (hs *HandlerSwitcher) GetHandler() (newHandler *mux.Router) {
|
||||
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.handler.Set(newHandler)
|
||||
}
|
@@ -1,18 +1,55 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"log"
|
||||
"bufio"
|
||||
"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 +62,136 @@ 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) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
return lirw.rw.(http.Hijacker).Hijack()
|
||||
}
|
||||
|
||||
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) {
|
||||
}
|
31
middlewares/rewrite.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/vulcand/vulcand/plugin/rewrite"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Rewrite is a middleware that allows redirections
|
||||
type Rewrite struct {
|
||||
rewriter *rewrite.Rewrite
|
||||
}
|
||||
|
||||
// NewRewrite creates a Rewrite middleware
|
||||
func NewRewrite(regex, replacement string, redirect bool) (*Rewrite, error) {
|
||||
rewriter, err := rewrite.NewRewrite(regex, replacement, false, redirect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Rewrite{rewriter: rewriter}, nil
|
||||
}
|
||||
|
||||
//
|
||||
func (rewrite *Rewrite) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
handler, err := rewrite.rewriter.NewHandler(next)
|
||||
if err != nil {
|
||||
log.Error("Error in rewrite middleware ", err)
|
||||
return
|
||||
}
|
||||
handler.ServeHTTP(rw, r)
|
||||
}
|
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
|
||||
}
|
@@ -1,52 +0,0 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/mailgun/oxy/roundrobin"
|
||||
)
|
||||
|
||||
// WebsocketUpgrader holds Websocket configuration.
|
||||
type WebsocketUpgrader struct {
|
||||
rr *roundrobin.RoundRobin
|
||||
}
|
||||
|
||||
// NewWebsocketUpgrader returns a new WebsocketUpgrader.
|
||||
func NewWebsocketUpgrader(rr *roundrobin.RoundRobin) *WebsocketUpgrader {
|
||||
wu := WebsocketUpgrader{
|
||||
rr: rr,
|
||||
}
|
||||
return &wu
|
||||
}
|
||||
|
||||
func (u *WebsocketUpgrader) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// If request is websocket, serve with golang websocket server to do protocol handshake
|
||||
if strings.Join(req.Header["Upgrade"], "") == "websocket" {
|
||||
start := time.Now().UTC()
|
||||
url, err := u.rr.NextServer()
|
||||
if err != nil {
|
||||
log.Errorf("Can't round robin in websocket middleware")
|
||||
return
|
||||
}
|
||||
log.Debugf("Websocket forward to %s", url.String())
|
||||
NewProxy(url).ServeHTTP(w, req)
|
||||
|
||||
if req.TLS != nil {
|
||||
log.Debugf("Round trip: %v, duration: %v tls:version: %x, tls:resume:%t, tls:csuite:%x, tls:server:%v",
|
||||
req.URL, time.Now().UTC().Sub(start),
|
||||
req.TLS.Version,
|
||||
req.TLS.DidResume,
|
||||
req.TLS.CipherSuite,
|
||||
req.TLS.ServerName)
|
||||
} else {
|
||||
log.Debugf("Round trip: %v, duration: %v",
|
||||
req.URL, time.Now().UTC().Sub(start))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
u.rr.ServeHTTP(w, req)
|
||||
}
|
@@ -1,179 +0,0 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// Original developpement made by https://github.com/koding/websocketproxy
|
||||
var (
|
||||
// DefaultUpgrader specifies the parameters for upgrading an HTTP
|
||||
// connection to a WebSocket connection.
|
||||
DefaultUpgrader = &websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
}
|
||||
|
||||
// DefaultDialer is a dialer with all fields set to the default zero values.
|
||||
DefaultDialer = websocket.DefaultDialer
|
||||
)
|
||||
|
||||
// WebsocketProxy is an HTTP Handler that takes an incoming WebSocket
|
||||
// connection and proxies it to another server.
|
||||
type WebsocketProxy struct {
|
||||
// Backend returns the backend URL which the proxy uses to reverse proxy
|
||||
// the incoming WebSocket connection. Request is the initial incoming and
|
||||
// unmodified request.
|
||||
Backend func(*http.Request) *url.URL
|
||||
|
||||
// Upgrader specifies the parameters for upgrading a incoming HTTP
|
||||
// connection to a WebSocket connection. If nil, DefaultUpgrader is used.
|
||||
Upgrader *websocket.Upgrader
|
||||
|
||||
// Dialer contains options for connecting to the backend WebSocket server.
|
||||
// If nil, DefaultDialer is used.
|
||||
Dialer *websocket.Dialer
|
||||
}
|
||||
|
||||
// ProxyHandler returns a new http.Handler interface that reverse proxies the
|
||||
// request to the given target.
|
||||
func ProxyHandler(target *url.URL) http.Handler {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
NewProxy(target).ServeHTTP(rw, req)
|
||||
})
|
||||
}
|
||||
|
||||
// NewProxy returns a new Websocket reverse proxy that rewrites the
|
||||
// URL's to the scheme, host and base path provider in target.
|
||||
func NewProxy(target *url.URL) *WebsocketProxy {
|
||||
backend := func(r *http.Request) *url.URL {
|
||||
// Shallow copy
|
||||
u := *target
|
||||
u.Fragment = r.URL.Fragment
|
||||
u.Path = r.URL.Path
|
||||
u.RawQuery = r.URL.RawQuery
|
||||
rurl := u.String()
|
||||
if strings.HasPrefix(rurl, "http") {
|
||||
u.Scheme = "ws"
|
||||
}
|
||||
if strings.HasPrefix(rurl, "https") {
|
||||
u.Scheme = "wss"
|
||||
}
|
||||
return &u
|
||||
}
|
||||
return &WebsocketProxy{Backend: backend}
|
||||
}
|
||||
|
||||
// ServeHTTP implements the http.Handler that proxies WebSocket connections.
|
||||
func (w *WebsocketProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
if w.Backend == nil {
|
||||
log.Errorf("Websocketproxy: backend function is not defined")
|
||||
http.Error(rw, "Backend not found", http.StatusInternalServerError)
|
||||
http.NotFound(rw, req)
|
||||
return
|
||||
}
|
||||
|
||||
backendURL := w.Backend(req)
|
||||
if backendURL == nil {
|
||||
log.Errorf("Websocketproxy: backend URL is nil")
|
||||
http.Error(rw, "Backend URL is nil", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
dialer := w.Dialer
|
||||
if w.Dialer == nil {
|
||||
dialer = DefaultDialer
|
||||
}
|
||||
|
||||
// Pass headers from the incoming request to the dialer to forward them to
|
||||
// the final destinations.
|
||||
requestHeader := http.Header{}
|
||||
requestHeader.Add("Origin", req.Header.Get("Origin"))
|
||||
for _, prot := range req.Header[http.CanonicalHeaderKey("Sec-WebSocket-Protocol")] {
|
||||
requestHeader.Add("Sec-WebSocket-Protocol", prot)
|
||||
}
|
||||
for _, cookie := range req.Header[http.CanonicalHeaderKey("Cookie")] {
|
||||
requestHeader.Add("Cookie", cookie)
|
||||
}
|
||||
for _, auth := range req.Header[http.CanonicalHeaderKey("Authorization")] {
|
||||
requestHeader.Add("Authorization", auth)
|
||||
}
|
||||
|
||||
// Pass X-Forwarded-For headers too, code below is a part of
|
||||
// httputil.ReverseProxy. See http://en.wikipedia.org/wiki/X-Forwarded-For
|
||||
// for more information
|
||||
// TODO: use RFC7239 http://tools.ietf.org/html/rfc7239
|
||||
if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
||||
// If we aren't the first proxy retain prior
|
||||
// X-Forwarded-For information as a comma+space
|
||||
// separated list and fold multiple headers into one.
|
||||
if prior, ok := req.Header["X-Forwarded-For"]; ok {
|
||||
clientIP = strings.Join(prior, ", ") + ", " + clientIP
|
||||
}
|
||||
requestHeader.Set("X-Forwarded-For", clientIP)
|
||||
}
|
||||
|
||||
// Set the originating protocol of the incoming HTTP request. The SSL might
|
||||
// be terminated on our site and because we doing proxy adding this would
|
||||
// be helpful for applications on the backend.
|
||||
requestHeader.Set("X-Forwarded-Proto", "http")
|
||||
if req.TLS != nil {
|
||||
requestHeader.Set("X-Forwarded-Proto", "https")
|
||||
}
|
||||
|
||||
//frontend Origin != backend Origin
|
||||
requestHeader.Del("Origin")
|
||||
|
||||
// Connect to the backend URL, also pass the headers we get from the requst
|
||||
// together with the Forwarded headers we prepared above.
|
||||
// TODO: support multiplexing on the same backend connection instead of
|
||||
// opening a new TCP connection time for each request. This should be
|
||||
// optional:
|
||||
// http://tools.ietf.org/html/draft-ietf-hybi-websocket-multiplexing-01
|
||||
connBackend, resp, err := dialer.Dial(backendURL.String(), requestHeader)
|
||||
if err != nil {
|
||||
log.Errorf("Websocketproxy: couldn't dial to remote backend url %s, %s, %+v", backendURL.String(), err, resp)
|
||||
http.Error(rw, "Remote backend unreachable", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer connBackend.Close()
|
||||
|
||||
upgrader := w.Upgrader
|
||||
if w.Upgrader == nil {
|
||||
upgrader = DefaultUpgrader
|
||||
}
|
||||
|
||||
// Only pass those headers to the upgrader.
|
||||
upgradeHeader := http.Header{}
|
||||
upgradeHeader.Set("Sec-WebSocket-Protocol",
|
||||
resp.Header.Get(http.CanonicalHeaderKey("Sec-WebSocket-Protocol")))
|
||||
upgradeHeader.Set("Set-Cookie",
|
||||
resp.Header.Get(http.CanonicalHeaderKey("Set-Cookie")))
|
||||
|
||||
// Now upgrade the existing incoming request to a WebSocket connection.
|
||||
// Also pass the header that we gathered from the Dial handshake.
|
||||
connPub, err := upgrader.Upgrade(rw, req, upgradeHeader)
|
||||
if err != nil {
|
||||
log.Errorf("Websocketproxy: couldn't upgrade %s", err)
|
||||
http.NotFound(rw, req)
|
||||
return
|
||||
}
|
||||
defer connPub.Close()
|
||||
|
||||
errc := make(chan error, 2)
|
||||
cp := func(dst io.Writer, src io.Reader) {
|
||||
_, err := io.Copy(dst, src)
|
||||
errc <- err
|
||||
}
|
||||
|
||||
// Start our proxy now, everything is ready...
|
||||
go cp(connBackend.UnderlyingConn(), connPub.UnderlyingConn())
|
||||
go cp(connPub.UnderlyingConn(), connBackend.UnderlyingConn())
|
||||
<-errc
|
||||
}
|
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
|
829
mocks/Marathon.go
Normal file
@@ -0,0 +1,829 @@
|
||||
package mocks
|
||||
|
||||
import "github.com/gambol99/go-marathon"
|
||||
import "github.com/stretchr/testify/mock"
|
||||
|
||||
import "net/url"
|
||||
|
||||
import "time"
|
||||
|
||||
// Marathon is a mock of marathon.Marathon
|
||||
type Marathon struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// ListApplications provides a mock function with given fields: _a0
|
||||
func (_m *Marathon) ListApplications(_a0 url.Values) ([]string, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func(url.Values) []string); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(url.Values) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ApplicationVersions provides a mock function with given fields: name
|
||||
func (_m *Marathon) ApplicationVersions(name string) (*marathon.ApplicationVersions, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *marathon.ApplicationVersions
|
||||
if rf, ok := ret.Get(0).(func(string) *marathon.ApplicationVersions); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.ApplicationVersions)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// HasApplicationVersion provides a mock function with given fields: name, version
|
||||
func (_m *Marathon) HasApplicationVersion(name string, version string) (bool, error) {
|
||||
ret := _m.Called(name, version)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
|
||||
r0 = rf(name, version)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(name, version)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetApplicationVersion provides a mock function with given fields: name, version
|
||||
func (_m *Marathon) SetApplicationVersion(name string, version *marathon.ApplicationVersion) (*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(name, version)
|
||||
|
||||
var r0 *marathon.DeploymentID
|
||||
if rf, ok := ret.Get(0).(func(string, *marathon.ApplicationVersion) *marathon.DeploymentID); ok {
|
||||
r0 = rf(name, version)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.DeploymentID)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *marathon.ApplicationVersion) error); ok {
|
||||
r1 = rf(name, version)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ApplicationOK provides a mock function with given fields: name
|
||||
func (_m *Marathon) ApplicationOK(name string) (bool, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CreateApplication provides a mock function with given fields: application
|
||||
func (_m *Marathon) CreateApplication(application *marathon.Application) (*marathon.Application, error) {
|
||||
ret := _m.Called(application)
|
||||
|
||||
var r0 *marathon.Application
|
||||
if rf, ok := ret.Get(0).(func(*marathon.Application) *marathon.Application); ok {
|
||||
r0 = rf(application)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.Application)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*marathon.Application) error); ok {
|
||||
r1 = rf(application)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// DeleteApplication provides a mock function with given fields: name
|
||||
func (_m *Marathon) DeleteApplication(name string) (*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *marathon.DeploymentID
|
||||
if rf, ok := ret.Get(0).(func(string) *marathon.DeploymentID); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.DeploymentID)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdateApplication provides a mock function with given fields: application
|
||||
func (_m *Marathon) UpdateApplication(application *marathon.Application) (*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(application)
|
||||
|
||||
var r0 *marathon.DeploymentID
|
||||
if rf, ok := ret.Get(0).(func(*marathon.Application) *marathon.DeploymentID); ok {
|
||||
r0 = rf(application)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.DeploymentID)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*marathon.Application) error); ok {
|
||||
r1 = rf(application)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ApplicationDeployments provides a mock function with given fields: name
|
||||
func (_m *Marathon) ApplicationDeployments(name string) ([]*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 []*marathon.DeploymentID
|
||||
if rf, ok := ret.Get(0).(func(string) []*marathon.DeploymentID); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*marathon.DeploymentID)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ScaleApplicationInstances provides a mock function with given fields: name, instances, force
|
||||
func (_m *Marathon) ScaleApplicationInstances(name string, instances int, force bool) (*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(name, instances, force)
|
||||
|
||||
var r0 *marathon.DeploymentID
|
||||
if rf, ok := ret.Get(0).(func(string, int, bool) *marathon.DeploymentID); ok {
|
||||
r0 = rf(name, instances, force)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.DeploymentID)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, int, bool) error); ok {
|
||||
r1 = rf(name, instances, force)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// RestartApplication provides a mock function with given fields: name, force
|
||||
func (_m *Marathon) RestartApplication(name string, force bool) (*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(name, force)
|
||||
|
||||
var r0 *marathon.DeploymentID
|
||||
if rf, ok := ret.Get(0).(func(string, bool) *marathon.DeploymentID); ok {
|
||||
r0 = rf(name, force)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.DeploymentID)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
|
||||
r1 = rf(name, force)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Applications provides a mock function with given fields: _a0
|
||||
func (_m *Marathon) Applications(_a0 url.Values) (*marathon.Applications, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *marathon.Applications
|
||||
if rf, ok := ret.Get(0).(func(url.Values) *marathon.Applications); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.Applications)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(url.Values) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Application provides a mock function with given fields: name
|
||||
func (_m *Marathon) Application(name string) (*marathon.Application, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *marathon.Application
|
||||
if rf, ok := ret.Get(0).(func(string) *marathon.Application); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.Application)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WaitOnApplication provides a mock function with given fields: name, timeout
|
||||
func (_m *Marathon) WaitOnApplication(name string, timeout time.Duration) error {
|
||||
ret := _m.Called(name, timeout)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, time.Duration) error); ok {
|
||||
r0 = rf(name, timeout)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Tasks provides a mock function with given fields: application
|
||||
func (_m *Marathon) Tasks(application string) (*marathon.Tasks, error) {
|
||||
ret := _m.Called(application)
|
||||
|
||||
var r0 *marathon.Tasks
|
||||
if rf, ok := ret.Get(0).(func(string) *marathon.Tasks); ok {
|
||||
r0 = rf(application)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.Tasks)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(application)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// AllTasks provides a mock function with given fields: opts
|
||||
func (_m *Marathon) AllTasks(opts *marathon.AllTasksOpts) (*marathon.Tasks, error) {
|
||||
ret := _m.Called(opts)
|
||||
|
||||
var r0 *marathon.Tasks
|
||||
if rf, ok := ret.Get(0).(func(*marathon.AllTasksOpts) *marathon.Tasks); ok {
|
||||
r0 = rf(opts)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.Tasks)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*marathon.AllTasksOpts) error); ok {
|
||||
r1 = rf(opts)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// TaskEndpoints provides a mock function with given fields: name, port, healthCheck
|
||||
func (_m *Marathon) TaskEndpoints(name string, port int, healthCheck bool) ([]string, error) {
|
||||
ret := _m.Called(name, port, healthCheck)
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func(string, int, bool) []string); ok {
|
||||
r0 = rf(name, port, healthCheck)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, int, bool) error); ok {
|
||||
r1 = rf(name, port, healthCheck)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// KillApplicationTasks provides a mock function with given fields: applicationID, opts
|
||||
func (_m *Marathon) KillApplicationTasks(applicationID string, opts *marathon.KillApplicationTasksOpts) (*marathon.Tasks, error) {
|
||||
ret := _m.Called(applicationID, opts)
|
||||
|
||||
var r0 *marathon.Tasks
|
||||
if rf, ok := ret.Get(0).(func(string, *marathon.KillApplicationTasksOpts) *marathon.Tasks); ok {
|
||||
r0 = rf(applicationID, opts)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.Tasks)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *marathon.KillApplicationTasksOpts) error); ok {
|
||||
r1 = rf(applicationID, opts)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// KillTask provides a mock function with given fields: taskID, opts
|
||||
func (_m *Marathon) KillTask(taskID string, opts *marathon.KillTaskOpts) (*marathon.Task, error) {
|
||||
ret := _m.Called(taskID, opts)
|
||||
|
||||
var r0 *marathon.Task
|
||||
if rf, ok := ret.Get(0).(func(string, *marathon.KillTaskOpts) *marathon.Task); ok {
|
||||
r0 = rf(taskID, opts)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.Task)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *marathon.KillTaskOpts) error); ok {
|
||||
r1 = rf(taskID, opts)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// KillTasks provides a mock function with given fields: taskIDs, opts
|
||||
func (_m *Marathon) KillTasks(taskIDs []string, opts *marathon.KillTaskOpts) error {
|
||||
ret := _m.Called(taskIDs, opts)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func([]string, *marathon.KillTaskOpts) error); ok {
|
||||
r0 = rf(taskIDs, opts)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Groups provides a mock function with given fields:
|
||||
func (_m *Marathon) Groups() (*marathon.Groups, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *marathon.Groups
|
||||
if rf, ok := ret.Get(0).(func() *marathon.Groups); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.Groups)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Group provides a mock function with given fields: name
|
||||
func (_m *Marathon) Group(name string) (*marathon.Group, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *marathon.Group
|
||||
if rf, ok := ret.Get(0).(func(string) *marathon.Group); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.Group)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CreateGroup provides a mock function with given fields: group
|
||||
func (_m *Marathon) CreateGroup(group *marathon.Group) error {
|
||||
ret := _m.Called(group)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*marathon.Group) error); ok {
|
||||
r0 = rf(group)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteGroup provides a mock function with given fields: name
|
||||
func (_m *Marathon) DeleteGroup(name string) (*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *marathon.DeploymentID
|
||||
if rf, ok := ret.Get(0).(func(string) *marathon.DeploymentID); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.DeploymentID)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UpdateGroup provides a mock function with given fields: id, group
|
||||
func (_m *Marathon) UpdateGroup(id string, group *marathon.Group) (*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(id, group)
|
||||
|
||||
var r0 *marathon.DeploymentID
|
||||
if rf, ok := ret.Get(0).(func(string, *marathon.Group) *marathon.DeploymentID); ok {
|
||||
r0 = rf(id, group)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.DeploymentID)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *marathon.Group) error); ok {
|
||||
r1 = rf(id, group)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// HasGroup provides a mock function with given fields: name
|
||||
func (_m *Marathon) HasGroup(name string) (bool, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
||||
r0 = rf(name)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(name)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WaitOnGroup provides a mock function with given fields: name, timeout
|
||||
func (_m *Marathon) WaitOnGroup(name string, timeout time.Duration) error {
|
||||
ret := _m.Called(name, timeout)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, time.Duration) error); ok {
|
||||
r0 = rf(name, timeout)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Deployments provides a mock function with given fields:
|
||||
func (_m *Marathon) Deployments() ([]*marathon.Deployment, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 []*marathon.Deployment
|
||||
if rf, ok := ret.Get(0).(func() []*marathon.Deployment); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*marathon.Deployment)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// DeleteDeployment provides a mock function with given fields: id, force
|
||||
func (_m *Marathon) DeleteDeployment(id string, force bool) (*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(id, force)
|
||||
|
||||
var r0 *marathon.DeploymentID
|
||||
if rf, ok := ret.Get(0).(func(string, bool) *marathon.DeploymentID); ok {
|
||||
r0 = rf(id, force)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.DeploymentID)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
|
||||
r1 = rf(id, force)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// HasDeployment provides a mock function with given fields: id
|
||||
func (_m *Marathon) HasDeployment(id string) (bool, error) {
|
||||
ret := _m.Called(id)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string) bool); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// WaitOnDeployment provides a mock function with given fields: id, timeout
|
||||
func (_m *Marathon) WaitOnDeployment(id string, timeout time.Duration) error {
|
||||
ret := _m.Called(id, timeout)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, time.Duration) error); ok {
|
||||
r0 = rf(id, timeout)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Subscriptions provides a mock function with given fields:
|
||||
func (_m *Marathon) Subscriptions() (*marathon.Subscriptions, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *marathon.Subscriptions
|
||||
if rf, ok := ret.Get(0).(func() *marathon.Subscriptions); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.Subscriptions)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// AddEventsListener provides a mock function with given fields: channel, filter
|
||||
func (_m *Marathon) AddEventsListener(channel marathon.EventsChannel, filter int) error {
|
||||
ret := _m.Called(channel, filter)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(marathon.EventsChannel, int) error); ok {
|
||||
r0 = rf(channel, filter)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// RemoveEventsListener provides a mock function with given fields: channel
|
||||
func (_m *Marathon) RemoveEventsListener(channel marathon.EventsChannel) {
|
||||
_m.Called(channel)
|
||||
}
|
||||
|
||||
// Unsubscribe provides a mock function with given fields: _a0
|
||||
func (_m *Marathon) Unsubscribe(_a0 string) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetMarathonURL provides a mock function with given fields:
|
||||
func (_m *Marathon) GetMarathonURL() string {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Ping provides a mock function with given fields:
|
||||
func (_m *Marathon) Ping() (bool, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Info provides a mock function with given fields:
|
||||
func (_m *Marathon) Info() (*marathon.Info, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *marathon.Info
|
||||
if rf, ok := ret.Get(0).(func() *marathon.Info); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*marathon.Info)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Leader provides a mock function with given fields:
|
||||
func (_m *Marathon) Leader() (string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// AbdicateLeader provides a mock function with given fields:
|
||||
func (_m *Marathon) AbdicateLeader() (string, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func() string); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
@@ -1,9 +1,10 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/boltdb"
|
||||
"github.com/emilevauge/traefik/types"
|
||||
)
|
||||
|
||||
// BoltDb holds configurations of the BoltDb provider.
|
||||
@@ -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 {
|
||||
provider.StoreType = store.BOLTDB
|
||||
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,9 +1,10 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/consul"
|
||||
"github.com/emilevauge/traefik/types"
|
||||
)
|
||||
|
||||
// Consul holds configurations of the Consul provider.
|
||||
@@ -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 {
|
||||
provider.StoreType = store.CONSUL
|
||||
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)
|
||||
}
|
||||
|
274
provider/consul_catalog.go
Normal file
@@ -0,0 +1,274 @@
|
||||
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"
|
||||
)
|
||||
|
||||
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.
|
||||
type ConsulCatalog struct {
|
||||
BaseProvider
|
||||
Endpoint string `description:"Consul server endpoint"`
|
||||
Domain string `description:"Default domain used"`
|
||||
client *api.Client
|
||||
Prefix string
|
||||
}
|
||||
|
||||
type serviceUpdate struct {
|
||||
ServiceName string
|
||||
Attributes []string
|
||||
}
|
||||
|
||||
type catalogUpdate struct {
|
||||
Service *serviceUpdate
|
||||
Nodes []*api.ServiceEntry
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) watchServices(stopCh <-chan struct{}) <-chan map[string][]string {
|
||||
watchCh := make(chan map[string][]string)
|
||||
|
||||
catalog := provider.client.Catalog()
|
||||
|
||||
safe.Go(func() {
|
||||
defer close(watchCh)
|
||||
|
||||
opts := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
data, meta, err := catalog.Services(opts)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to list services")
|
||||
return
|
||||
}
|
||||
|
||||
// If LastIndex didn't change then it means `Get` returned
|
||||
// because of the WaitTime and the key didn't changed.
|
||||
if opts.WaitIndex == meta.LastIndex {
|
||||
continue
|
||||
}
|
||||
opts.WaitIndex = meta.LastIndex
|
||||
|
||||
if data != nil {
|
||||
watchCh <- data
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return watchCh
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) healthyNodes(service string) (catalogUpdate, error) {
|
||||
health := provider.client.Health()
|
||||
opts := &api.QueryOptions{}
|
||||
data, _, err := health.Service(service, "", true, opts)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("Failed to fetch details of " + service)
|
||||
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: &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) 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)
|
||||
|
||||
for _, tag := range node.Service.Tags {
|
||||
serviceName += "--" + normalize(tag)
|
||||
}
|
||||
|
||||
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,
|
||||
"getFrontendRule": provider.getFrontendRule,
|
||||
"getBackendName": provider.getBackendName,
|
||||
"getBackendAddress": provider.getBackendAddress,
|
||||
"getAttribute": provider.getAttribute,
|
||||
"getEntryPoints": provider.getEntryPoints,
|
||||
}
|
||||
|
||||
allNodes := []*api.ServiceEntry{}
|
||||
services := []*serviceUpdate{}
|
||||
for _, info := range catalog {
|
||||
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 []*serviceUpdate
|
||||
Nodes []*api.ServiceEntry
|
||||
}{
|
||||
Services: services,
|
||||
Nodes: allNodes,
|
||||
}
|
||||
|
||||
configuration, err := provider.getConfiguration("templates/consul_catalog.tmpl", FuncMap, templateObjects)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("Failed to create config")
|
||||
}
|
||||
|
||||
return configuration
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) getNodes(index map[string][]string) ([]catalogUpdate, error) {
|
||||
visited := make(map[string]bool)
|
||||
|
||||
nodes := []catalogUpdate{}
|
||||
for service := range index {
|
||||
name := strings.ToLower(service)
|
||||
if !strings.Contains(name, " ") && !visited[name] {
|
||||
visited[name] = true
|
||||
log.WithFields(log.Fields{
|
||||
"service": name,
|
||||
}).Debug("Fetching service")
|
||||
healthy, err := provider.healthyNodes(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nodes = append(nodes, healthy)
|
||||
}
|
||||
}
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) watch(configurationChan chan<- types.ConfigMessage, stop chan bool) error {
|
||||
stopCh := make(chan struct{})
|
||||
serviceCatalog := provider.watchServices(stopCh)
|
||||
|
||||
defer close(stopCh)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return nil
|
||||
case index, ok := <-serviceCatalog:
|
||||
if !ok {
|
||||
return errors.New("Consul service list nil")
|
||||
}
|
||||
log.Debug("List of services changed")
|
||||
nodes, err := provider.getNodes(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configuration := provider.buildConfig(nodes)
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "consul_catalog",
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *ConsulCatalog) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
|
||||
config := api.DefaultConfig()
|
||||
config.Address = provider.Endpoint
|
||||
client, err := api.NewClient(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provider.client = client
|
||||
|
||||
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, stop)
|
||||
}
|
||||
err := backoff.RetryNotify(worker, backoff.NewExponentialBackOff(), notify)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot connect to consul server %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
276
provider/consul_catalog_test.go
Normal file
@@ -0,0 +1,276 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/hashicorp/consul/api"
|
||||
)
|
||||
|
||||
func TestConsulCatalogGetFrontendRule(t *testing.T) {
|
||||
provider := &ConsulCatalog{
|
||||
Domain: "localhost",
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
service serviceUpdate
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
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.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",
|
||||
},
|
||||
{
|
||||
node: &api.ServiceEntry{
|
||||
Service: &api.AgentService{
|
||||
Service: "api",
|
||||
Address: "10.0.0.1",
|
||||
Port: 80,
|
||||
Tags: []string{"a funny looking tag"},
|
||||
},
|
||||
},
|
||||
expected: "api--10-0-0-1--80--a-funny-looking-tag--2",
|
||||
},
|
||||
}
|
||||
|
||||
for i, e := range services {
|
||||
actual := provider.getBackendName(e.node, i)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsulCatalogBuildConfig(t *testing.T) {
|
||||
provider := &ConsulCatalog{
|
||||
Domain: "localhost",
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
nodes []catalogUpdate
|
||||
expectedFrontends map[string]*types.Frontend
|
||||
expectedBackends map[string]*types.Backend
|
||||
}{
|
||||
{
|
||||
nodes: []catalogUpdate{},
|
||||
expectedFrontends: map[string]*types.Frontend{},
|
||||
expectedBackends: map[string]*types.Backend{},
|
||||
},
|
||||
{
|
||||
nodes: []catalogUpdate{
|
||||
{
|
||||
Service: &serviceUpdate{
|
||||
ServiceName: "test",
|
||||
Attributes: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedFrontends: map[string]*types.Frontend{},
|
||||
expectedBackends: map[string]*types.Backend{},
|
||||
},
|
||||
{
|
||||
nodes: []catalogUpdate{
|
||||
{
|
||||
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",
|
||||
Address: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedFrontends: map[string]*types.Frontend{
|
||||
"frontend-test": {
|
||||
Backend: "backend-test",
|
||||
PassHostHeader: true,
|
||||
Routes: map[string]types.Route{
|
||||
"route-host-test": {
|
||||
Rule: "Host:test.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedBackends: map[string]*types.Backend{
|
||||
"backend-test": {
|
||||
Servers: map[string]types.Server{
|
||||
"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: &types.CircuitBreaker{
|
||||
Expression: "NetworkErrorRatio() > 0.5",
|
||||
},
|
||||
LoadBalancer: &types.LoadBalancer{
|
||||
Method: "drr",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actualConfig := provider.buildConfig(c.nodes)
|
||||
if !reflect.DeepEqual(actualConfig.Backends, c.expectedBackends) {
|
||||
t.Fatalf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends)
|
||||
}
|
||||
if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) {
|
||||
t.Fatalf("expected %#v, got %#v", c.expectedFrontends, actualConfig.Frontends)
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,104 +2,162 @@ 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/emilevauge/traefik/types"
|
||||
"github.com/fsouza/go-dockerclient"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"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
|
||||
Endpoint string
|
||||
Domain string
|
||||
TLS *DockerTLS
|
||||
BaseProvider
|
||||
Endpoint string `description:"Docker server endpoint. Can be a tcp or a unix socket endpoint"`
|
||||
Domain string `description:"Default domain used"`
|
||||
TLS *DockerTLS `description:"Enable Docker TLS support"`
|
||||
}
|
||||
|
||||
// DockerTLS holds TLS specific configurations
|
||||
type DockerTLS struct {
|
||||
CA string
|
||||
Cert string
|
||||
Key string
|
||||
InsecureSkipVerify bool
|
||||
CA string `description:"TLS CA"`
|
||||
Cert string `description:"TLS cert"`
|
||||
Key string `description:"TLS key"`
|
||||
InsecureSkipVerify bool `description:"TLS insecure skip verify"`
|
||||
}
|
||||
|
||||
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 {
|
||||
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 err 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)
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create a client for docker, error: %s", err)
|
||||
return err
|
||||
}
|
||||
err = dockerClient.Ping()
|
||||
if err != nil {
|
||||
log.Errorf("Docker connection error %+v", err)
|
||||
return err
|
||||
}
|
||||
log.Debug("Docker connection established")
|
||||
if provider.Watch {
|
||||
dockerEvents := make(chan *docker.APIEvents)
|
||||
dockerClient.AddEventListener(dockerEvents)
|
||||
log.Debug("Docker listening")
|
||||
go func() {
|
||||
operation := func() error {
|
||||
for {
|
||||
event := <-dockerEvents
|
||||
if event == nil {
|
||||
return errors.New("Docker event nil")
|
||||
// log.Fatalf("Docker connection error")
|
||||
dockerClient, err := provider.createClient()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create a client for docker, error: %s", err)
|
||||
return err
|
||||
}
|
||||
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("Failed to list containers for docker, error %s", err)
|
||||
return err
|
||||
}
|
||||
configuration := provider.loadDockerConfig(containers)
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "docker",
|
||||
Configuration: configuration,
|
||||
}
|
||||
if provider.Watch {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Docker connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot connect to docker server %+v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
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
|
||||
}
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Docker connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, backoff.NewExponentialBackOff(), notify)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot connect to docker server %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
configuration := provider.loadDockerConfig(listContainers(dockerClient))
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "docker",
|
||||
Configuration: configuration,
|
||||
}
|
||||
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,
|
||||
@@ -107,22 +165,22 @@ func (provider *Docker) loadDockerConfig(containersInspected []docker.Container)
|
||||
"getDomain": provider.getDomain,
|
||||
"getProtocol": provider.getProtocol,
|
||||
"getPassHostHeader": provider.getPassHostHeader,
|
||||
"getFrontendValue": provider.getFrontendValue,
|
||||
"getEntryPoints": provider.getEntryPoints,
|
||||
"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,
|
||||
@@ -137,12 +195,12 @@ func (provider *Docker) loadDockerConfig(containersInspected []docker.Container)
|
||||
return configuration
|
||||
}
|
||||
|
||||
func containerFilter(container docker.Container) bool {
|
||||
if len(container.NetworkSettings.Ports) == 0 {
|
||||
log.Debugf("Filtering container without port %s", container.Name)
|
||||
func containerFilter(container dockertypes.ContainerJSON) bool {
|
||||
_, err := strconv.Atoi(container.Config.Labels["traefik.port"])
|
||||
if len(container.NetworkSettings.Ports) == 0 && err != nil {
|
||||
log.Debugf("Filtering container without port and no traefik.port label %s", container.Name)
|
||||
return false
|
||||
}
|
||||
_, err := strconv.Atoi(container.Config.Labels["traefik.port"])
|
||||
if len(container.NetworkSettings.Ports) > 1 && err != nil {
|
||||
log.Debugf("Filtering container with more than 1 port and no traefik.port label %s", container.Name)
|
||||
return false
|
||||
@@ -153,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
|
||||
}
|
||||
@@ -206,35 +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"
|
||||
return "true"
|
||||
}
|
||||
|
||||
func getLabel(container docker.Container, label string) (string, error) {
|
||||
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 dockertypes.ContainerJSON, label string) (string, error) {
|
||||
for key, value := range container.Config.Labels {
|
||||
if key == label {
|
||||
return value, nil
|
||||
@@ -243,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 {
|
||||
@@ -259,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
|
||||
}
|
||||
|