Compare commits
206 Commits
v1.0.alpha
...
v1.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
784fd74d3f | ||
|
|
cfbd43d1ee | ||
|
|
f10bbd8c69 | ||
|
|
6bcb6f92f5 | ||
|
|
f6b5684a5b | ||
|
|
866e8db5f7 | ||
|
|
a9925c7521 | ||
|
|
f955cc33c5 | ||
|
|
e728f32a15 | ||
|
|
4abb4c6489 | ||
|
|
66998e60b8 | ||
|
|
71288e5799 | ||
|
|
8fdd0b20d1 | ||
|
|
4e9ff45747 | ||
|
|
d6e28a923c | ||
|
|
1604786285 | ||
|
|
35cb9100cd | ||
|
|
4729e3e999 | ||
|
|
b0e66a4aa6 | ||
|
|
4218467ab3 | ||
|
|
6e62625ebf | ||
|
|
c8a0a83e2b | ||
|
|
76bd04e349 | ||
|
|
a8a78b8ea3 | ||
|
|
3435ebfe42 | ||
|
|
4d485e1b6b |
@@ -1,3 +1,5 @@
|
||||
dist/
|
||||
vendor/
|
||||
!dist/traefik
|
||||
site/
|
||||
**/*.test
|
||||
|
||||
78
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
# 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
|
||||
```
|
||||
4
.gitignore
vendored
@@ -1,9 +1,13 @@
|
||||
/dist
|
||||
gen.go
|
||||
.idea
|
||||
.intellij
|
||||
log
|
||||
*.iml
|
||||
traefik
|
||||
traefik.toml
|
||||
*.test
|
||||
vendor/
|
||||
static/
|
||||
.vscode/
|
||||
site/
|
||||
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
|
||||
28
.travis.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
branches:
|
||||
except:
|
||||
- /^v\d\.\d\.\d.*$/
|
||||
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: v1.0.0-beta.$TRAVIS_BUILD_NUMBER
|
||||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
install:
|
||||
- sudo service docker stop
|
||||
- sudo curl https://get.docker.com/builds/Linux/x86_64/docker-1.10.1 -o /usr/bin/docker
|
||||
- sudo chmod +x /usr/bin/docker
|
||||
- sudo service docker start
|
||||
- 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,4 +1,5 @@
|
||||
FROM scratch
|
||||
COPY script/ca-certificates.crt /etc/ssl/certs/
|
||||
COPY dist/traefik /
|
||||
EXPOSE 80
|
||||
ENTRYPOINT ["/traefik"]
|
||||
|
||||
@@ -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.
|
||||
|
||||
62
Makefile
@@ -4,63 +4,60 @@ 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_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 .
|
||||
|
||||
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 +67,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)
|
||||
|
||||
150
README.md
@@ -1,32 +1,65 @@
|
||||

|
||||
___
|
||||
|
||||
[](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/), [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
|
||||
|
||||
@@ -43,114 +76,65 @@ 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 awsome 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 -c 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.
|
||||
## Træfɪk here and there
|
||||
|
||||
#### 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`.
|
||||
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)
|
||||
|
||||
#### Using `Docker` and `Makefile`
|
||||
- Project [Mantl](https://mantl.io/) from Cisco
|
||||
|
||||
You need to run the `binary` target. This will create binaries for
|
||||
linux platform in the `dist` folder.
|
||||

|
||||
> 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.
|
||||
|
||||
```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 [Apollo](http://capgemini.github.io/devops/apollo/) from Cap Gemini
|
||||
|
||||
---> Making bundle: binary (in .)
|
||||

|
||||
> 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.
|
||||
|
||||
$ ls dist/
|
||||
traefik*
|
||||
```
|
||||
## Partners
|
||||
|
||||
#### Using `glide`
|
||||
[](https://zenika.com)
|
||||
|
||||
The idea behind `glide` is the following :
|
||||
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.
|
||||
|
||||
- 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`.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
### Tests
|
||||
[](https://aster.is)
|
||||
|
||||
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/emilevauge/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/emilevauge/traefik 0.005s coverage: 4.1% of statements
|
||||
|
||||
Test success
|
||||
```
|
||||
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.
|
||||
.
|
||||
|
||||
423
acme/acme.go
Normal file
@@ -0,0 +1,423 @@
|
||||
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"
|
||||
"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
|
||||
Domains []Domain
|
||||
StorageFile string
|
||||
OnDemand bool
|
||||
CAServer string
|
||||
EntryPoint string
|
||||
storageLock sync.RWMutex
|
||||
}
|
||||
|
||||
// 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 filenmae 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 := false
|
||||
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
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"net/http"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// OxyLogger implements oxy Logger interface with logrus.
|
||||
@@ -33,10 +32,3 @@ 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,13 +1,14 @@
|
||||
FROM golang:1.5
|
||||
FROM golang:1.6.0-alpine
|
||||
|
||||
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 apk update && apk add git bash gcc musl-dev \
|
||||
&& go get github.com/Masterminds/glide \
|
||||
&& go get github.com/mitchellh/gox \
|
||||
&& 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
|
||||
ENV DOCKER_VERSION 1.10.1
|
||||
|
||||
# enable GO15VENDOREXPERIMENT
|
||||
ENV GO15VENDOREXPERIMENT 1
|
||||
@@ -20,9 +21,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
|
||||
|
||||
219
cmd.go
Normal file
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
Copyright
|
||||
*/
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
fmtlog "log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/containous/traefik/middlewares"
|
||||
"github.com/containous/traefik/provider"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var traefikCmd = &cobra.Command{
|
||||
Use: "traefik",
|
||||
Short: "traefik, a modern reverse proxy",
|
||||
Long: `traefik is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
|
||||
Complete documentation is available at http://traefik.io`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
run()
|
||||
},
|
||||
}
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Print version",
|
||||
Long: `Print version`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmtlog.Println(Version + " built on the " + BuildDate)
|
||||
os.Exit(0)
|
||||
},
|
||||
}
|
||||
|
||||
var arguments = struct {
|
||||
GlobalConfiguration
|
||||
web bool
|
||||
file bool
|
||||
docker bool
|
||||
dockerTLS bool
|
||||
marathon bool
|
||||
consul bool
|
||||
consulTLS bool
|
||||
consulCatalog bool
|
||||
zookeeper bool
|
||||
etcd bool
|
||||
etcdTLS bool
|
||||
boltdb bool
|
||||
}{
|
||||
GlobalConfiguration{
|
||||
EntryPoints: make(EntryPoints),
|
||||
Docker: &provider.Docker{
|
||||
TLS: &provider.DockerTLS{},
|
||||
},
|
||||
File: &provider.File{},
|
||||
Web: &WebProvider{},
|
||||
Marathon: &provider.Marathon{},
|
||||
Consul: &provider.Consul{
|
||||
Kv: provider.Kv{
|
||||
TLS: &provider.KvTLS{},
|
||||
},
|
||||
},
|
||||
ConsulCatalog: &provider.ConsulCatalog{},
|
||||
Zookeeper: &provider.Zookepper{},
|
||||
Etcd: &provider.Etcd{
|
||||
Kv: provider.Kv{
|
||||
TLS: &provider.KvTLS{},
|
||||
},
|
||||
},
|
||||
Boltdb: &provider.BoltDb{},
|
||||
},
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
}
|
||||
|
||||
func init() {
|
||||
traefikCmd.AddCommand(versionCmd)
|
||||
traefikCmd.PersistentFlags().StringP("configFile", "c", "", "Configuration file to use (TOML, JSON, YAML, HCL).")
|
||||
traefikCmd.PersistentFlags().StringP("graceTimeOut", "g", "10", "Timeout in seconds. Duration to give active requests a chance to finish during hot-reloads")
|
||||
traefikCmd.PersistentFlags().String("accessLogsFile", "log/access.log", "Access logs file")
|
||||
traefikCmd.PersistentFlags().String("traefikLogsFile", "log/traefik.log", "Traefik logs file")
|
||||
traefikCmd.PersistentFlags().Var(&arguments.EntryPoints, "entryPoints", "Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'")
|
||||
traefikCmd.PersistentFlags().Var(&arguments.DefaultEntryPoints, "defaultEntryPoints", "Entrypoints to be used by frontends that do not specify any entrypoint")
|
||||
traefikCmd.PersistentFlags().StringP("logLevel", "l", "ERROR", "Log level")
|
||||
traefikCmd.PersistentFlags().DurationVar(&arguments.ProvidersThrottleDuration, "providersThrottleDuration", time.Duration(2*time.Second), "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.")
|
||||
traefikCmd.PersistentFlags().Int("maxIdleConnsPerHost", 0, "If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used")
|
||||
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.web, "web", false, "Enable Web backend")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Web.Address, "web.address", ":8080", "Web administration port")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Web.CertFile, "web.cerFile", "", "SSL certificate")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Web.KeyFile, "web.keyFile", "", "SSL certificate")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Web.ReadOnly, "web.readOnly", false, "Enable read only API")
|
||||
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.file, "file", false, "Enable File backend")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.File.Watch, "file.watch", true, "Watch provider")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.File.Filename, "file.filename", "", "Override default configuration template. For advanced users :)")
|
||||
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.docker, "docker", false, "Enable Docker backend")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Docker.Watch, "docker.watch", true, "Watch provider")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.Filename, "docker.filename", "", "Override default configuration template. For advanced users :)")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.Endpoint, "docker.endpoint", "unix:///var/run/docker.sock", "Docker server endpoint. Can be a tcp or a unix socket endpoint")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.Domain, "docker.domain", "", "Default domain used")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.dockerTLS, "docker.tls", false, "Enable Docker TLS support")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.TLS.CA, "docker.tls.ca", "", "TLS CA")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.TLS.Cert, "docker.tls.cert", "", "TLS cert")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.TLS.Key, "docker.tls.key", "", "TLS key")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Docker.TLS.InsecureSkipVerify, "docker.tls.insecureSkipVerify", false, "TLS insecure skip verify")
|
||||
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.marathon, "marathon", false, "Enable Marathon backend")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Marathon.Watch, "marathon.watch", true, "Watch provider")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Marathon.Filename, "marathon.filename", "", "Override default configuration template. For advanced users :)")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Marathon.Endpoint, "marathon.endpoint", "http://127.0.0.1:8080", "Marathon server endpoint. You can also specify multiple endpoint for Marathon")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Marathon.Domain, "marathon.domain", "", "Default domain used")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Marathon.ExposedByDefault, "marathon.exposedByDefault", true, "Expose Marathon apps by default")
|
||||
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.consul, "consul", false, "Enable Consul backend")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Consul.Watch, "consul.watch", true, "Watch provider")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Consul.Filename, "consul.filename", "", "Override default configuration template. For advanced users :)")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Consul.Endpoint, "consul.endpoint", "127.0.0.1:8500", "Comma sepparated Consul server endpoints")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Consul.Prefix, "consul.prefix", "/traefik", "Prefix used for KV store")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.consulTLS, "consul.tls", false, "Enable Consul TLS support")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Consul.TLS.CA, "consul.tls.ca", "", "TLS CA")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Consul.TLS.Cert, "consul.tls.cert", "", "TLS cert")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Consul.TLS.Key, "consul.tls.key", "", "TLS key")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Consul.TLS.InsecureSkipVerify, "consul.tls.insecureSkipVerify", false, "TLS insecure skip verify")
|
||||
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.consulCatalog, "consulCatalog", false, "Enable Consul catalog backend")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Domain, "consulCatalog.domain", "", "Default domain used")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Endpoint, "consulCatalog.endpoint", "127.0.0.1:8500", "Consul server endpoint")
|
||||
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.zookeeper, "zookeeper", false, "Enable Zookeeper backend")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Zookeeper.Watch, "zookeeper.watch", true, "Watch provider")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Zookeeper.Filename, "zookeeper.filename", "", "Override default configuration template. For advanced users :)")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Zookeeper.Endpoint, "zookeeper.endpoint", "127.0.0.1:2181", "Comma sepparated Zookeeper server endpoints")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Zookeeper.Prefix, "zookeeper.prefix", "/traefik", "Prefix used for KV store")
|
||||
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.etcd, "etcd", false, "Enable Etcd backend")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Etcd.Watch, "etcd.watch", true, "Watch provider")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.Filename, "etcd.filename", "", "Override default configuration template. For advanced users :)")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.Endpoint, "etcd.endpoint", "127.0.0.1:4001", "Comma sepparated Etcd server endpoints")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.Prefix, "etcd.prefix", "/traefik", "Prefix used for KV store")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.etcdTLS, "etcd.tls", false, "Enable Etcd TLS support")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.TLS.CA, "etcd.tls.ca", "", "TLS CA")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.TLS.Cert, "etcd.tls.cert", "", "TLS cert")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.TLS.Key, "etcd.tls.key", "", "TLS key")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Etcd.TLS.InsecureSkipVerify, "etcd.tls.insecureSkipVerify", false, "TLS insecure skip verify")
|
||||
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.boltdb, "boltdb", false, "Enable Boltdb backend")
|
||||
traefikCmd.PersistentFlags().BoolVar(&arguments.Boltdb.Watch, "boltdb.watch", true, "Watch provider")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Filename, "boltdb.filename", "", "Override default configuration template. For advanced users :)")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Endpoint, "boltdb.endpoint", "127.0.0.1:4001", "Boltdb server endpoint")
|
||||
traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Prefix, "boltdb.prefix", "/traefik", "Prefix used for KV store")
|
||||
|
||||
_ = viper.BindPFlag("configFile", traefikCmd.PersistentFlags().Lookup("configFile"))
|
||||
_ = viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut"))
|
||||
_ = viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel"))
|
||||
// TODO: wait for this issue to be corrected: https://github.com/spf13/viper/issues/105
|
||||
_ = viper.BindPFlag("providersThrottleDuration", traefikCmd.PersistentFlags().Lookup("providersThrottleDuration"))
|
||||
_ = viper.BindPFlag("maxIdleConnsPerHost", traefikCmd.PersistentFlags().Lookup("maxIdleConnsPerHost"))
|
||||
viper.SetDefault("providersThrottleDuration", time.Duration(2*time.Second))
|
||||
viper.SetDefault("logLevel", "ERROR")
|
||||
viper.SetDefault("MaxIdleConnsPerHost", 200)
|
||||
}
|
||||
|
||||
func run() {
|
||||
fmtlog.SetFlags(fmtlog.Lshortfile | fmtlog.LstdFlags)
|
||||
|
||||
// load global configuration
|
||||
globalConfiguration := LoadConfiguration()
|
||||
|
||||
http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = globalConfiguration.MaxIdleConnsPerHost
|
||||
loggerMiddleware := middlewares.NewLogger(globalConfiguration.AccessLogsFile)
|
||||
defer loggerMiddleware.Close()
|
||||
|
||||
// logging
|
||||
level, err := log.ParseLevel(strings.ToLower(globalConfiguration.LogLevel))
|
||||
if err != nil {
|
||||
log.Fatal("Error getting level", err)
|
||||
}
|
||||
log.SetLevel(level)
|
||||
|
||||
if len(globalConfiguration.TraefikLogsFile) > 0 {
|
||||
fi, err := os.OpenFile(globalConfiguration.TraefikLogsFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
defer func() {
|
||||
if err := fi.Close(); err != nil {
|
||||
log.Error("Error closinf file", err)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
log.Fatal("Error opening file", err)
|
||||
} else {
|
||||
log.SetOutput(fi)
|
||||
log.SetFormatter(&log.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true})
|
||||
}
|
||||
} else {
|
||||
log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableSorting: true})
|
||||
}
|
||||
jsonConf, _ := json.Marshal(globalConfiguration)
|
||||
log.Debugf("Global configuration loaded %s", string(jsonConf))
|
||||
server := NewServer(*globalConfiguration)
|
||||
server.Start()
|
||||
defer server.Close()
|
||||
log.Info("Shutting down")
|
||||
}
|
||||
279
configuration.go
@@ -1,53 +1,298 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
fmtlog "log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/emilevauge/traefik/provider"
|
||||
"github.com/emilevauge/traefik/types"
|
||||
"github.com/containous/traefik/acme"
|
||||
"github.com/containous/traefik/provider"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// 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
|
||||
CertFile, KeyFile string
|
||||
LogLevel string
|
||||
EntryPoints EntryPoints
|
||||
ACME *acme.ACME
|
||||
DefaultEntryPoints DefaultEntryPoints
|
||||
ProvidersThrottleDuration time.Duration
|
||||
MaxIdleConnsPerHost int
|
||||
Retry *Retry
|
||||
Docker *provider.Docker
|
||||
File *provider.File
|
||||
Web *WebProvider
|
||||
Marathon *provider.Marathon
|
||||
Consul *provider.Consul
|
||||
ConsulCatalog *provider.ConsulCatalog
|
||||
Etcd *provider.Etcd
|
||||
Zookeeper *provider.Zookepper
|
||||
Boltdb *provider.BoltDb
|
||||
}
|
||||
|
||||
// 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)
|
||||
// DefaultEntryPoints holds default entry points
|
||||
type DefaultEntryPoints []string
|
||||
|
||||
return globalConfiguration
|
||||
// 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 fmt.Sprintf("%#v", dep)
|
||||
}
|
||||
|
||||
// LoadFileConfig returns a GlobalConfiguration from reading the specified file (a toml file).
|
||||
func LoadFileConfig(file string) *GlobalConfiguration {
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 ""
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
type Certificate struct {
|
||||
CertFile string
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
// Retry contains request retry config
|
||||
type Retry struct {
|
||||
Attempts int
|
||||
MaxMem int64
|
||||
}
|
||||
|
||||
// NewGlobalConfiguration returns a GlobalConfiguration with default values.
|
||||
func NewGlobalConfiguration() *GlobalConfiguration {
|
||||
return new(GlobalConfiguration)
|
||||
}
|
||||
|
||||
// LoadConfiguration returns a GlobalConfiguration.
|
||||
func LoadConfiguration() *GlobalConfiguration {
|
||||
configuration := NewGlobalConfiguration()
|
||||
if _, err := toml.DecodeFile(file, configuration); err != nil {
|
||||
viper.SetEnvPrefix("traefik")
|
||||
viper.SetConfigType("toml")
|
||||
viper.AutomaticEnv()
|
||||
if len(viper.GetString("configFile")) > 0 {
|
||||
viper.SetConfigFile(viper.GetString("configFile"))
|
||||
} else {
|
||||
viper.SetConfigName("traefik") // name of config file (without extension)
|
||||
}
|
||||
viper.AddConfigPath("/etc/traefik/") // path to look for the config file in
|
||||
viper.AddConfigPath("$HOME/.traefik/") // call multiple times to add many search paths
|
||||
viper.AddConfigPath(".") // optionally look for config in the working directory
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
fmtlog.Fatalf("Error reading file: %s", err)
|
||||
}
|
||||
|
||||
if len(arguments.EntryPoints) > 0 {
|
||||
viper.Set("entryPoints", arguments.EntryPoints)
|
||||
}
|
||||
if len(arguments.DefaultEntryPoints) > 0 {
|
||||
viper.Set("defaultEntryPoints", arguments.DefaultEntryPoints)
|
||||
}
|
||||
if arguments.web {
|
||||
viper.Set("web", arguments.Web)
|
||||
}
|
||||
if arguments.file {
|
||||
viper.Set("file", arguments.File)
|
||||
}
|
||||
if !arguments.dockerTLS {
|
||||
arguments.Docker.TLS = nil
|
||||
}
|
||||
if arguments.docker {
|
||||
viper.Set("docker", arguments.Docker)
|
||||
}
|
||||
if arguments.marathon {
|
||||
viper.Set("marathon", arguments.Marathon)
|
||||
}
|
||||
if !arguments.consulTLS {
|
||||
arguments.Consul.TLS = nil
|
||||
}
|
||||
if arguments.consul {
|
||||
viper.Set("consul", arguments.Consul)
|
||||
}
|
||||
if arguments.consulCatalog {
|
||||
viper.Set("consulCatalog", arguments.ConsulCatalog)
|
||||
}
|
||||
if arguments.zookeeper {
|
||||
viper.Set("zookeeper", arguments.Zookeeper)
|
||||
}
|
||||
if !arguments.etcdTLS {
|
||||
arguments.Etcd.TLS = nil
|
||||
}
|
||||
if arguments.etcd {
|
||||
viper.Set("etcd", arguments.Etcd)
|
||||
}
|
||||
if arguments.boltdb {
|
||||
viper.Set("boltdb", arguments.Boltdb)
|
||||
}
|
||||
if err := unmarshal(&configuration); err != nil {
|
||||
|
||||
fmtlog.Fatalf("Error reading file: %s", err)
|
||||
}
|
||||
|
||||
if len(configuration.EntryPoints) == 0 {
|
||||
configuration.EntryPoints = make(map[string]*EntryPoint)
|
||||
configuration.EntryPoints["http"] = &EntryPoint{
|
||||
Address: ":80",
|
||||
}
|
||||
configuration.DefaultEntryPoints = []string{"http"}
|
||||
}
|
||||
|
||||
if configuration.File != nil && len(configuration.File.Filename) == 0 {
|
||||
// no filename, setting to global config file
|
||||
configuration.File.Filename = viper.ConfigFileUsed()
|
||||
}
|
||||
|
||||
return configuration
|
||||
}
|
||||
|
||||
func unmarshal(rawVal interface{}) error {
|
||||
config := &mapstructure.DecoderConfig{
|
||||
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
|
||||
Metadata: nil,
|
||||
Result: rawVal,
|
||||
WeaklyTypedInput: true,
|
||||
}
|
||||
|
||||
decoder, err := mapstructure.NewDecoder(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = decoder.Decode(viper.AllSettings())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
182
docs/basics.md
Normal file
@@ -0,0 +1,182 @@
|
||||
|
||||
# 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 forwared 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 expries, 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)
|
||||
|
||||
## Servers
|
||||
|
||||
Servers are simply defined using a `URL`. You can also apply a custom `weight` to each server (this will be used by load-balacning).
|
||||
|
||||
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
|
||||
```
|
||||
212
docs/benchmarks.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Benchmarks
|
||||
|
||||
## Configuration
|
||||
|
||||
I would like to thanks [vincentbernat](https://github.com/vincentbernat) from [exoscale.ch](https://www.exoscale.ch) who kindly provided the infrastructure needed for the benchmarks.
|
||||
|
||||
I used 4 VMs for the tests with the following configuration:
|
||||
|
||||
- 32 GB RAM
|
||||
- 8 CPU Cores
|
||||
- 10 GB SSD
|
||||
- Ubuntu 14.04 LTS 64-bit
|
||||
|
||||
## Setup
|
||||
|
||||
1. One VM used to launch the benchmarking tool [wrk](https://github.com/wg/wrk)
|
||||
2. One VM for traefik (v1.0.0-beta.416) / nginx (v1.4.6)
|
||||
3. Two VMs for 2 backend servers in go [whoami](https://github.com/emilevauge/whoamI/)
|
||||
|
||||
Each VM has been tuned using the following limits:
|
||||
|
||||
```bash
|
||||
sysctl -w fs.file-max="9999999"
|
||||
sysctl -w fs.nr_open="9999999"
|
||||
sysctl -w net.core.netdev_max_backlog="4096"
|
||||
sysctl -w net.core.rmem_max="16777216"
|
||||
sysctl -w net.core.somaxconn="65535"
|
||||
sysctl -w net.core.wmem_max="16777216"
|
||||
sysctl -w net.ipv4.ip_local_port_range="1025 65535"
|
||||
sysctl -w net.ipv4.tcp_fin_timeout="30"
|
||||
sysctl -w net.ipv4.tcp_keepalive_time="30"
|
||||
sysctl -w net.ipv4.tcp_max_syn_backlog="20480"
|
||||
sysctl -w net.ipv4.tcp_max_tw_buckets="400000"
|
||||
sysctl -w net.ipv4.tcp_no_metrics_save="1"
|
||||
sysctl -w net.ipv4.tcp_syn_retries="2"
|
||||
sysctl -w net.ipv4.tcp_synack_retries="2"
|
||||
sysctl -w net.ipv4.tcp_tw_recycle="1"
|
||||
sysctl -w net.ipv4.tcp_tw_reuse="1"
|
||||
sysctl -w vm.min_free_kbytes="65536"
|
||||
sysctl -w vm.overcommit_memory="1"
|
||||
ulimit -n 9999999
|
||||
```
|
||||
|
||||
### Nginx
|
||||
|
||||
Here is the config Nginx file use `/etc/nginx/nginx.conf`:
|
||||
|
||||
```
|
||||
user www-data;
|
||||
worker_processes auto;
|
||||
worker_rlimit_nofile 200000;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 10000;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 300;
|
||||
keepalive_requests 10000;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
open_file_cache max=200000 inactive=300s;
|
||||
open_file_cache_valid 300s;
|
||||
open_file_cache_min_uses 2;
|
||||
open_file_cache_errors on;
|
||||
|
||||
server_tokens off;
|
||||
dav_methods off;
|
||||
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
access_log /var/log/nginx/access.log combined;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
gzip off;
|
||||
gzip_vary off;
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
include /etc/nginx/sites-enabled/*.conf;
|
||||
}
|
||||
```
|
||||
|
||||
Here is the Nginx vhost file used:
|
||||
|
||||
```
|
||||
upstream whoami {
|
||||
server IP-whoami1:80;
|
||||
server IP-whoami2:80;
|
||||
keepalive 300;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8001;
|
||||
server_name test.traefik;
|
||||
access_log off;
|
||||
error_log /dev/null crit;
|
||||
if ($host != "test.traefik") {
|
||||
return 404;
|
||||
}
|
||||
location / {
|
||||
proxy_pass http://whoami;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Traefik
|
||||
|
||||
Here is the `traefik.toml` file used:
|
||||
|
||||
```
|
||||
MaxIdleConnsPerHost = 100000
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
[file]
|
||||
[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.servers.server1]
|
||||
url = "http://IP-whoami1:80"
|
||||
weight = 1
|
||||
[backends.backend1.servers.server2]
|
||||
url = "http://IP-whoami2:80"
|
||||
weight = 1
|
||||
|
||||
[frontends]
|
||||
[frontends.frontend1]
|
||||
backend = "backend1"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host: test.traefik"
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
### whoami:
|
||||
```
|
||||
wrk -t8 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-whoami:80/bench
|
||||
Running 1m test @ http://IP-whoami:80/bench
|
||||
20 threads and 1000 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 70.28ms 134.72ms 1.91s 89.94%
|
||||
Req/Sec 2.92k 742.42 8.78k 68.80%
|
||||
Latency Distribution
|
||||
50% 10.63ms
|
||||
75% 75.64ms
|
||||
90% 205.65ms
|
||||
99% 668.28ms
|
||||
3476705 requests in 1.00m, 384.61MB read
|
||||
Socket errors: connect 0, read 0, write 0, timeout 103
|
||||
Requests/sec: 57894.35
|
||||
Transfer/sec: 6.40MB
|
||||
```
|
||||
|
||||
### nginx:
|
||||
```
|
||||
wrk -t20 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-nginx:8001/bench
|
||||
Running 1m test @ http://IP-nginx:8001/bench
|
||||
20 threads and 1000 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 101.25ms 180.09ms 1.99s 89.34%
|
||||
Req/Sec 1.69k 567.69 9.39k 72.62%
|
||||
Latency Distribution
|
||||
50% 15.46ms
|
||||
75% 129.11ms
|
||||
90% 302.44ms
|
||||
99% 846.59ms
|
||||
2018427 requests in 1.00m, 298.36MB read
|
||||
Socket errors: connect 0, read 0, write 0, timeout 90
|
||||
Requests/sec: 33591.67
|
||||
Transfer/sec: 4.97MB
|
||||
```
|
||||
|
||||
### traefik:
|
||||
```
|
||||
wrk -t8 -c1000 -d60s -H "Host: test.traefik" --latency http://IP-traefik:8000/bench
|
||||
Running 1m test @ http://IP-traefik:8000/bench
|
||||
20 threads and 1000 connections
|
||||
Thread Stats Avg Stdev Max +/- Stdev
|
||||
Latency 91.72ms 150.43ms 2.00s 90.50%
|
||||
Req/Sec 1.43k 266.37 2.97k 69.77%
|
||||
Latency Distribution
|
||||
50% 19.74ms
|
||||
75% 121.98ms
|
||||
90% 237.39ms
|
||||
99% 687.49ms
|
||||
1705073 requests in 1.00m, 188.63MB read
|
||||
Socket errors: connect 0, read 0, write 0, timeout 7
|
||||
Requests/sec: 28392.44
|
||||
Transfer/sec: 3.14MB
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
Traefik is obviously slower than Nginx, but not so much: Traefik can serve 28392 requests/sec and Nginx 33591 requests/sec which gives a ratio of 85%.
|
||||
Not bad for young project :) !
|
||||
|
||||
Some areas of possible improvements:
|
||||
- Use [GO_REUSEPORT](https://github.com/kavu/go_reuseport) listener
|
||||
- Run a separate server instance per CPU core with `GOMAXPROCS=1` (it appears during benchmarks that there is a lot more context switches with traefik than with nginx)
|
||||
|
||||
43
docs/css/traefik.css
Normal file
@@ -0,0 +1,43 @@
|
||||
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;
|
||||
}
|
||||
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 |
953
docs/index.md
@@ -1,890 +1,131 @@
|
||||

|
||||
___
|
||||
<p align="center">
|
||||
<img src="img/traefik.logo.png" alt="Træfɪk" title="Træfɪk" />
|
||||
</p>
|
||||
|
||||
|
||||
# <a id="top"></a> Documentation
|
||||
|
||||
- [Basics](#basics)
|
||||
- [Global configuration](#global)
|
||||
- [File backend](#file)
|
||||
- [API backend](#api)
|
||||
- [Docker backend](#docker)
|
||||
- [Mesos/Marathon backend](#marathon)
|
||||
- [Consul backend](#consul)
|
||||
- [Etcd backend](#etcd)
|
||||
- [Zookeeper backend](#zk)
|
||||
- [Boltdb backend](#boltdb)
|
||||
- [Benchmarks](#benchmarks)
|
||||
|
||||
|
||||
## <a id="basics"></a> Basics
|
||||
[](https://travis-ci.org/containous/traefik)
|
||||
[](https://docs.traefik.io)
|
||||
[](http://goreportcard.com/report/containous/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/), 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/), [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.
|
||||
|
||||
Basically, Træfɪk is a http router, which sends traffic from frontends to http backends, following rules you have configured.
|
||||
## Overview
|
||||
|
||||
### <a id="frontends"></a> Frontends
|
||||
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:
|
||||
|
||||
Frontends can be defined using the following rules:
|
||||
- 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
|
||||
|
||||
- `Headers`: Headers adds a matcher for request header values. It accepts a sequence of key/value pairs to be matched. For example: `application/json`
|
||||
- `HeadersRegexp`: Regular expressions can be used with headers as well. It accepts a sequence of key/value pairs, where the value has regex support. For example: `application/(text|json)`
|
||||
- `Host`: Host adds a matcher for the URL host. It accepts a template with zero or more URL variables enclosed by `{}`. Variables can define an optional regexp pattern to be matched: `www.traefik.io`, `{subdomain:[a-z]+}.traefik.io`
|
||||
- `Methods`: Methods adds a matcher for HTTP methods. It accepts a sequence of one or more methods to be matched, e.g.: `GET`, `POST`, `PUT`
|
||||
- `Path`: Path adds a matcher for the URL path. It accepts a template with zero or more URL variables enclosed by `{}`. The template must start with a `/`. For exemple `/products/` `/articles/{category}/{id:[0-9]+}`
|
||||
- `PathPrefix`: PathPrefix adds a matcher for the URL path prefix. This matches if the given template is a prefix of the full URL path.
|
||||
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.
|
||||
|
||||
A frontend is a set of rules that forwards the incoming http traffic to a backend.
|
||||
You can optionally enable `passHostHeader` to forward client `Host` header to the backend.
|
||||
Here enters Træfɪk.
|
||||
|
||||
### HTTP 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:
|
||||
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.
|
||||
|
||||
- `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.
|
||||
Run it and forget it!
|
||||
|
||||
|
||||
A circuit breaker can also be applied to a backend, preventing high loads on failing servers.
|
||||
It can be configured using:
|
||||
## Demo
|
||||
|
||||
- Methods: `LatencyAtQuantileMS`, `NetworkErrorRatio`, `ResponseCodeRatio`
|
||||
- Operators: `AND`, `OR`, `EQ`, `NEQ`, `LT`, `LE`, `GT`, `GE`
|
||||
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.
|
||||
|
||||
For example:
|
||||
- `NetworkErrorRatio() > 0.5`
|
||||
- `LatencyAtQuantileMS(50.0) > 50`
|
||||
- `ResponseCodeRatio(500, 600, 0, 600) > 0.5`
|
||||
[](https://asciinema.org/a/4tcyde7riou5vxulo6my3mtko)
|
||||
|
||||
## <a id="global"></a> Global configuration
|
||||
## Get it
|
||||
|
||||
```toml
|
||||
# traefik.toml
|
||||
################################################################
|
||||
# Global configuration
|
||||
################################################################
|
||||
### Binary
|
||||
|
||||
# Reverse proxy port
|
||||
#
|
||||
# Optional
|
||||
# Default: ":80"
|
||||
#
|
||||
# port = ":80"
|
||||
|
||||
# Timeout in seconds.
|
||||
# Duration to give active requests a chance to finish during hot-reloads
|
||||
#
|
||||
# Optional
|
||||
# Default: 10
|
||||
#
|
||||
# graceTimeOut = 10
|
||||
|
||||
# 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"
|
||||
|
||||
# SSL certificate and key used
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# CertFile = "traefik.crt"
|
||||
# KeyFile = "traefik.key"
|
||||
|
||||
# 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"
|
||||
You can 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 -c traefik.toml
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
## <a id="file"></a> File backend
|
||||
Using the tiny Docker image:
|
||||
|
||||
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
|
||||
port = ":80"
|
||||
graceTimeOut = 10
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[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.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"
|
||||
value = "test.localhost"
|
||||
[frontends.frontend2]
|
||||
backend = "backend1"
|
||||
passHostHeader = true
|
||||
[frontends.frontend2.routes.test_2]
|
||||
rule = "Path"
|
||||
value = "/test"
|
||||
```shell
|
||||
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml traefik
|
||||
```
|
||||
|
||||
- or put your rules in a separate file, for example `rules.tml`:
|
||||
## Test it
|
||||
|
||||
```toml
|
||||
# traefik.toml
|
||||
port = ":80"
|
||||
graceTimeOut = 10
|
||||
logLevel = "DEBUG"
|
||||
You can test Træfɪk easily using [Docker compose](https://docs.docker.com/compose), with this `docker-compose.yml` file:
|
||||
|
||||
[file]
|
||||
filename = "rules.toml"
|
||||
```yaml
|
||||
traefik:
|
||||
image: traefik
|
||||
command: --web --docker --docker.domain=docker.localhost --logLevel=DEBUG
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /dev/null:/traefik.toml
|
||||
|
||||
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"
|
||||
```
|
||||
|
||||
```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.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"
|
||||
value = "test.localhost"
|
||||
[frontends.frontend2]
|
||||
backend = "backend1"
|
||||
passHostHeader = true
|
||||
[frontends.frontend2.routes.test_2]
|
||||
rule = "Path"
|
||||
value = "/test"
|
||||
|
||||
Then, start it:
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
If you want Træfɪk to watch file changes automatically, just add:
|
||||
Finally, test load-balancing between the two servers `whoami1` and `whoami2`:
|
||||
|
||||
```toml
|
||||
[file]
|
||||
watch = true
|
||||
```
|
||||
|
||||
## <a id="api"></a> 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"
|
||||
```
|
||||
|
||||
- `/`: 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": {
|
||||
"value": "/test",
|
||||
"rule": "Path"
|
||||
}
|
||||
},
|
||||
"backend": "backend1"
|
||||
},
|
||||
"frontend1": {
|
||||
"routes": {
|
||||
"test_1": {
|
||||
"value": "test.localhost",
|
||||
"rule": "Host"
|
||||
}
|
||||
},
|
||||
"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
|
||||
|
||||
|
||||
## <a id="docker"></a> 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"
|
||||
```
|
||||
|
||||
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`: override the default frontend rule (Default: Host). See [frontends](#frontends).
|
||||
- `traefik.frontend.value=test.example.com`: override the default frontend value (Default: `{containerName}.{domain}`) See [frontends](#frontends).
|
||||
- `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend.
|
||||
* `traefik.domain=traefik.localhost`: override the default domain
|
||||
|
||||
|
||||
## <a id="marathon"></a> 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"
|
||||
|
||||
# Network interface used to call Marathon web services
|
||||
# Optional
|
||||
# Default: "eth0"
|
||||
#
|
||||
# networkInterface = "eth0"
|
||||
|
||||
# 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"
|
||||
```
|
||||
|
||||
Labels can be used on containers to override default behaviour:
|
||||
|
||||
- `traefik.backend=foo`: assign the application to `foo` backend
|
||||
- `traefik.port=80`: register this port. Useful when the application exposes multiples ports.
|
||||
- `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`: override the default frontend rule (Default: Host). See [frontends](#frontends).
|
||||
- `traefik.frontend.value=test.example.com`: override the default frontend value (Default: `{appName}.{domain}`) See [frontends](#frontends).
|
||||
- `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend.
|
||||
* `traefik.domain=traefik.localhost`: override the default domain
|
||||
|
||||
## <a id="consul"></a> 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 = "http://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"
|
||||
```
|
||||
|
||||
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/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` |
|
||||
| `/traefik/frontends/frontend1/routes/test_1/value` | `test.localhost` |
|
||||
|
||||
- frontend 2
|
||||
|
||||
| Key | Value |
|
||||
|----------------------------------------------------|------------|
|
||||
| `/traefik/frontends/frontend2/backend` | `backend1` |
|
||||
| `/traefik/frontends/frontend2/passHostHeader` | `true` |
|
||||
| `/traefik/frontends/frontend2/routes/test_2/rule` | `Path` |
|
||||
| `/traefik/frontends/frontend2/routes/test_2/value` | `/test` |
|
||||
|
||||
|
||||
## <a id="etcd"></a> 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"
|
||||
```
|
||||
|
||||
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/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` |
|
||||
| `/traefik/frontends/frontend1/routes/test_1/value` | `test.localhost` |
|
||||
|
||||
- frontend 2
|
||||
|
||||
| Key | Value |
|
||||
|----------------------------------------------------|------------|
|
||||
| `/traefik/frontends/frontend2/backend` | `backend1` |
|
||||
| `/traefik/frontends/frontend2/passHostHeader` | `true` |
|
||||
| `/traefik/frontends/frontend2/routes/test_2/rule` | `Path` |
|
||||
| `/traefik/frontends/frontend2/routes/test_2/value` | `/test` |
|
||||
|
||||
|
||||
## <a id="zk"></a> 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"
|
||||
```
|
||||
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/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` |
|
||||
| `/traefik/frontends/frontend1/routes/test_1/value | `test.localhost` |
|
||||
|
||||
- frontend 2
|
||||
|
||||
| Key | Value |
|
||||
|----------------------------------------------------|------------|
|
||||
| `/traefik/frontends/frontend2/backend` | `backend1` |
|
||||
| `/traefik/frontends/frontend2/passHostHeader` | `true` |
|
||||
| `/traefik/frontends/frontend2/routes/test_2/rule` | `Path` |
|
||||
| `/traefik/frontends/frontend2/routes/test_2/value` | `/test` |
|
||||
|
||||
|
||||
## <a id="boltdb"></a> 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"
|
||||
```
|
||||
|
||||
|
||||
## <a id="benchmarks"></a> Benchmarks
|
||||
|
||||
Here are some early Benchmarks between Nginx and Træfɪk acting as simple load balancers between two servers.
|
||||
|
||||
- Nginx:
|
||||
|
||||
```sh
|
||||
$ docker run -d -e VIRTUAL_HOST=test1.localhost emilevauge/whoami
|
||||
$ docker run -d -e VIRTUAL_HOST=test1.localhost emilevauge/whoami
|
||||
$ docker run --log-driver=none -d -p 80:80 -v /var/run/docker.sock:/tmp/docker.sock:ro jwilder/nginx-proxy
|
||||
$ ab -n 20000 -c 20 -r http://test1.localhost/
|
||||
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
|
||||
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
||||
Licensed to The Apache Software Foundation, http://www.apache.org/
|
||||
|
||||
Benchmarking test1.localhost (be patient)
|
||||
Completed 2000 requests
|
||||
Completed 4000 requests
|
||||
Completed 6000 requests
|
||||
Completed 8000 requests
|
||||
Completed 10000 requests
|
||||
Completed 12000 requests
|
||||
Completed 14000 requests
|
||||
Completed 16000 requests
|
||||
Completed 18000 requests
|
||||
Completed 20000 requests
|
||||
Finished 20000 requests
|
||||
|
||||
|
||||
Server Software: nginx/1.9.2
|
||||
Server Hostname: test1.localhost
|
||||
Server Port: 80
|
||||
|
||||
Document Path: /
|
||||
Document Length: 287 bytes
|
||||
|
||||
Concurrency Level: 20
|
||||
Time taken for tests: 5.874 seconds
|
||||
Complete requests: 20000
|
||||
Failed requests: 0
|
||||
Total transferred: 8900000 bytes
|
||||
HTML transferred: 5740000 bytes
|
||||
Requests per second: 3404.97 [#/sec] (mean)
|
||||
Time per request: 5.874 [ms] (mean)
|
||||
Time per request: 0.294 [ms] (mean, across all concurrent requests)
|
||||
Transfer rate: 1479.70 [Kbytes/sec] received
|
||||
|
||||
Connection Times (ms)
|
||||
min mean[+/-sd] median max
|
||||
Connect: 0 0 0.1 0 2
|
||||
Processing: 0 6 2.4 6 35
|
||||
Waiting: 0 5 2.3 5 33
|
||||
Total: 0 6 2.4 6 36
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 6
|
||||
66% 6
|
||||
75% 7
|
||||
80% 7
|
||||
90% 9
|
||||
95% 10
|
||||
98% 12
|
||||
99% 13
|
||||
100% 36 (longest request)
|
||||
```
|
||||
|
||||
- Træfɪk:
|
||||
|
||||
```sh
|
||||
$ docker run -d -l traefik.backend=test1 -l traefik.host=test1 emilevauge/whoami
|
||||
$ docker run -d -l traefik.backend=test1 -l traefik.host=test1 emilevauge/whoami
|
||||
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/traefik.toml -v /var/run/docker.sock:/var/run/docker.sock emilevauge/traefik
|
||||
$ ab -n 20000 -c 20 -r http://test1.docker.localhost/
|
||||
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
|
||||
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
|
||||
Licensed to The Apache Software Foundation, http://www.apache.org/
|
||||
|
||||
Benchmarking test1.docker.localhost (be patient)
|
||||
Completed 2000 requests
|
||||
Completed 4000 requests
|
||||
Completed 6000 requests
|
||||
Completed 8000 requests
|
||||
Completed 10000 requests
|
||||
Completed 12000 requests
|
||||
Completed 14000 requests
|
||||
Completed 16000 requests
|
||||
Completed 18000 requests
|
||||
Completed 20000 requests
|
||||
Finished 20000 requests
|
||||
|
||||
|
||||
Server Software: .
|
||||
Server Hostname: test1.docker.localhost
|
||||
Server Port: 80
|
||||
|
||||
Document Path: /
|
||||
Document Length: 312 bytes
|
||||
|
||||
Concurrency Level: 20
|
||||
Time taken for tests: 6.545 seconds
|
||||
Complete requests: 20000
|
||||
Failed requests: 0
|
||||
Total transferred: 8600000 bytes
|
||||
HTML transferred: 6240000 bytes
|
||||
Requests per second: 3055.60 [#/sec] (mean)
|
||||
Time per request: 6.545 [ms] (mean)
|
||||
Time per request: 0.327 [ms] (mean, across all concurrent requests)
|
||||
Transfer rate: 1283.11 [Kbytes/sec] received
|
||||
|
||||
Connection Times (ms)
|
||||
min mean[+/-sd] median max
|
||||
Connect: 0 0 0.2 0 7
|
||||
Processing: 1 6 2.2 6 22
|
||||
Waiting: 1 6 2.1 6 21
|
||||
Total: 1 7 2.2 6 22
|
||||
|
||||
Percentage of the requests served within a certain time (ms)
|
||||
50% 6
|
||||
66% 7
|
||||
75% 8
|
||||
80% 8
|
||||
90% 9
|
||||
95% 10
|
||||
98% 11
|
||||
99% 13
|
||||
100% 22 (longest request)
|
||||
```bash
|
||||
$ curl -H Host:whoami.docker.localhost http://127.0.0.1
|
||||
Hostname: ef194d07634a
|
||||
IP: 127.0.0.1
|
||||
IP: ::1
|
||||
IP: 172.17.0.4
|
||||
IP: fe80::42:acff:fe11:4
|
||||
GET / HTTP/1.1
|
||||
Host: 172.17.0.4:80
|
||||
User-Agent: curl/7.35.0
|
||||
Accept: */*
|
||||
Accept-Encoding: gzip
|
||||
X-Forwarded-For: 172.17.0.1
|
||||
X-Forwarded-Host: 172.17.0.4:80
|
||||
X-Forwarded-Proto: http
|
||||
X-Forwarded-Server: dbb60406010d
|
||||
|
||||
$ curl -H Host:whoami.docker.localhost http://127.0.0.1
|
||||
Hostname: 6c3c5df0c79a
|
||||
IP: 127.0.0.1
|
||||
IP: ::1
|
||||
IP: 172.17.0.3
|
||||
IP: fe80::42:acff:fe11:3
|
||||
GET / HTTP/1.1
|
||||
Host: 172.17.0.3:80
|
||||
User-Agent: curl/7.35.0
|
||||
Accept: */*
|
||||
Accept-Encoding: gzip
|
||||
X-Forwarded-For: 172.17.0.1
|
||||
X-Forwarded-Host: 172.17.0.3:80
|
||||
X-Forwarded-Proto: http
|
||||
X-Forwarded-Server: dbb60406010d
|
||||
```
|
||||
|
||||
994
docs/toml.md
Normal file
@@ -0,0 +1,994 @@
|
||||
|
||||
# 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"
|
||||
```
|
||||
|
||||
## 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
|
||||
# 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
|
||||
# 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"
|
||||
```
|
||||
|
||||
# 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.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.tml`:
|
||||
|
||||
```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.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}`). See [frontends](#frontends).
|
||||
- `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}`). See [frontends](#frontends).
|
||||
- `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
|
||||
|
||||
## 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"
|
||||
```
|
||||
|
||||
This backend will create routes matching on hostname based on the service name
|
||||
used in consul.
|
||||
|
||||
|
||||
## 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/loadbalancer/method` | `drr` |
|
||||
| `/traefik/backends/backend2/servers/server1/url` | `http://172.17.0.4:80` |
|
||||
| `/traefik/backends/backend2/servers/server1/weight` | `1` |
|
||||
| `/traefik/backends/backend2/servers/server2/url` | `http://172.17.0.5:80` |
|
||||
| `/traefik/backends/backend2/servers/server2/weight` | `2` |
|
||||
|
||||
- frontend 1
|
||||
|
||||
| Key | Value |
|
||||
|---------------------------------------------------|-----------------------|
|
||||
| `/traefik/frontends/frontend1/backend` | `backend2` |
|
||||
| `/traefik/frontends/frontend1/routes/test_1/rule` | `Host:test.localhost` |
|
||||
|
||||
- frontend 2
|
||||
|
||||
| Key | Value |
|
||||
|----------------------------------------------------|--------------|
|
||||
| `/traefik/frontends/frontend2/backend` | `backend1` |
|
||||
| `/traefik/frontends/frontend2/passHostHeader` | `true` |
|
||||
| `/traefik/frontends/frontend2/entrypoints` | `http,https` |
|
||||
| `/traefik/frontends/frontend2/routes/test_2/rule` | `Path:/test` |
|
||||
|
||||
## Atomic configuration changes
|
||||
|
||||
The [Etcd](https://github.com/coreos/etcd/issues/860) and [Consul](https://github.com/hashicorp/consul/issues/886) backends do not support updating multiple keys atomically. As a result, it may be possible for Træfɪk to read an intermediate configuration state despite judicious use of the `--providersThrottleDuration` flag. To solve this problem, Træfɪk supports a special key called `/traefik/alias`. If set, Træfɪk use the value as an alternative key prefix.
|
||||
|
||||
Given the key structure below, Træfɪk will use the `http://172.17.0.2:80` as its only backend (frontend keys have been omitted for brevity).
|
||||
|
||||
| Key | Value |
|
||||
|-------------------------------------------------------------------------|-----------------------------|
|
||||
| `/traefik/alias` | `/traefik_configurations/1` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/weight` | `10` |
|
||||
|
||||
When an atomic configuration change is required, you may write a new configuration at an alternative prefix. Here, although the `/traefik_configurations/2/...` keys have been set, the old configuration is still active because the `/traefik/alias` key still points to `/traefik_configurations/1`:
|
||||
|
||||
| Key | Value |
|
||||
|-------------------------------------------------------------------------|-----------------------------|
|
||||
| `/traefik/alias` | `/traefik_configurations/1` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/weight` | `10` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server1/weight` | `5` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server2/url` | `http://172.17.0.3:80` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server2/weight` | `5` |
|
||||
|
||||
Once the `/traefik/alias` key is updated, the new `/traefik_configurations/2` configuration becomes active atomically. Here, we have a 50% balance between the `http://172.17.0.3:80` and the `http://172.17.0.4:80` hosts while no traffic is sent to the `172.17.0.2:80` host:
|
||||
|
||||
| Key | Value |
|
||||
|-------------------------------------------------------------------------|-----------------------------|
|
||||
| `/traefik/alias` | `/traefik_configurations/2` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
|
||||
| `/traefik_configurations/1/backends/backend1/servers/server1/weight` | `10` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server1/url` | `http://172.17.0.3:80` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server1/weight` | `5` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server2/url` | `http://172.17.0.4:80` |
|
||||
| `/traefik_configurations/2/backends/backend1/servers/server2/weight` | `5` |
|
||||
|
||||
Note that Træfɪk *will not watch for key changes in the `/traefik_configurations` prefix*. It will only watch for changes in the `/traefik` prefix. Further, if the `/traefik/alias` key is set, all other sibling keys with the `/traefik` prefix are ignored.
|
||||
|
||||
|
||||
# Examples
|
||||
|
||||
## 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"
|
||||
```
|
||||
@@ -6,7 +6,7 @@ zk:
|
||||
ZK_ID: 1
|
||||
|
||||
master:
|
||||
image: mesosphere/mesos-master:0.23.0-1.0.ubuntu1404
|
||||
image: mesosphere/mesos-master:0.26.0-0.2.145.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.26.0-0.2.145.ubuntu1404
|
||||
net: host
|
||||
pid: host
|
||||
privileged: true
|
||||
@@ -31,9 +31,10 @@ 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:v0.13.0
|
||||
net: host
|
||||
environment:
|
||||
MARATHON_MASTER: zk://127.0.0.1:2181/mesos
|
||||
21
examples/compose-traefik.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
traefik:
|
||||
image: traefik
|
||||
command: --web --docker --docker.domain=docker.localhost --logLevel=DEBUG
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /dev/null:/traefik.toml
|
||||
|
||||
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,12 @@ curl -i -H "Accept: application/json" -X PUT -d "2" ht
|
||||
|
||||
# frontend 1
|
||||
curl -i -H "Accept: application/json" -X PUT -d "backend2" http://localhost:8500/v1/kv/traefik/frontends/frontend1/backend
|
||||
curl -i -H "Accept: application/json" -X PUT -d "http" http://localhost:8500/v1/kv/traefik/frontends/frontend1/entrypoints
|
||||
curl -i -H "Accept: application/json" -X PUT -d "Host" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/rule
|
||||
curl -i -H "Accept: application/json" -X PUT -d "test.localhost" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/value
|
||||
|
||||
# frontend 2
|
||||
curl -i -H "Accept: application/json" -X PUT -d "backend1" http://localhost:8500/v1/kv/traefik/frontends/frontend2/backend
|
||||
curl -i -H "Accept: application/json" -X PUT -d "http,https" http://localhost:8500/v1/kv/traefik/frontends/frontend2/entrypoints
|
||||
curl -i -H "Accept: application/json" -X PUT -d "Path" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/rule
|
||||
curl -i -H "Accept: application/json" -X PUT -d "/test" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/value
|
||||
@@ -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.protocole": "http",
|
||||
"traefik.frontend.rule" : "Headers:Host,test.localhost"
|
||||
}
|
||||
}
|
||||
272
glide.lock
generated
Normal file
@@ -0,0 +1,272 @@
|
||||
hash: 79b6eb2a613b5e2ce5c57150eec41ac04def3f232a3613fd8b5a88b5e1041b38
|
||||
updated: 2016-04-02T15:42:37.505896092+02:00
|
||||
imports:
|
||||
- name: github.com/alecthomas/template
|
||||
version: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0
|
||||
- name: github.com/alecthomas/units
|
||||
version: 6b4e7dc5e3143b85ea77909c72caf89416fc2915
|
||||
- name: github.com/boltdb/bolt
|
||||
version: 51f99c862475898df9773747d3accd05a7ca33c1
|
||||
- name: github.com/BurntSushi/toml
|
||||
version: bd2bdf7f18f849530ef7a1c29a4290217cab32a1
|
||||
- name: github.com/BurntSushi/ty
|
||||
version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74
|
||||
subpackages:
|
||||
- fun
|
||||
- name: github.com/cenkalti/backoff
|
||||
version: 4dc77674aceaabba2c7e3da25d4c823edfb73f99
|
||||
- name: github.com/codahale/hdrhistogram
|
||||
version: 954f16e8b9ef0e5d5189456aa4c1202758e04f17
|
||||
- name: github.com/codegangsta/cli
|
||||
version: bf4a526f48af7badd25d2cb02d587e1b01be3b50
|
||||
- name: github.com/codegangsta/negroni
|
||||
version: c7477ad8e330bef55bf1ebe300cf8aa67c492d1b
|
||||
- name: github.com/containous/oxy
|
||||
version: 0b5b371bce661385d35439204298fa6fb5db5463
|
||||
subpackages:
|
||||
- cbreaker
|
||||
- forward
|
||||
- memmetrics
|
||||
- roundrobin
|
||||
- utils
|
||||
- stream
|
||||
- name: github.com/coreos/go-etcd
|
||||
version: cc90c7b091275e606ad0ca7102a23fb2072f3f5e
|
||||
subpackages:
|
||||
- etcd
|
||||
- name: github.com/davecgh/go-spew
|
||||
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
|
||||
subpackages:
|
||||
- spew
|
||||
- name: github.com/docker/distribution
|
||||
version: ff6f38ccb69afa96214c7ee955359465d1fc767a
|
||||
subpackages:
|
||||
- reference
|
||||
- name: github.com/docker/docker
|
||||
version: f39987afe8d611407887b3094c03d6ba6a766a67
|
||||
subpackages:
|
||||
- autogen
|
||||
- api
|
||||
- cliconfig
|
||||
- daemon/network
|
||||
- graph/tags
|
||||
- image
|
||||
- opts
|
||||
- pkg/archive
|
||||
- pkg/fileutils
|
||||
- pkg/homedir
|
||||
- pkg/httputils
|
||||
- pkg/ioutils
|
||||
- pkg/jsonmessage
|
||||
- pkg/mflag
|
||||
- pkg/nat
|
||||
- pkg/parsers
|
||||
- pkg/pools
|
||||
- pkg/promise
|
||||
- pkg/random
|
||||
- pkg/stdcopy
|
||||
- pkg/stringid
|
||||
- pkg/symlink
|
||||
- pkg/system
|
||||
- pkg/tarsum
|
||||
- pkg/term
|
||||
- pkg/timeutils
|
||||
- pkg/tlsconfig
|
||||
- pkg/ulimit
|
||||
- pkg/units
|
||||
- pkg/urlutil
|
||||
- pkg/useragent
|
||||
- pkg/version
|
||||
- registry
|
||||
- runconfig
|
||||
- utils
|
||||
- volume
|
||||
- name: github.com/docker/engine-api
|
||||
version: 8924d6900370b4c7e7984be5adc61f50a80d7537
|
||||
subpackages:
|
||||
- client
|
||||
- types
|
||||
- types/container
|
||||
- types/filters
|
||||
- types/strslice
|
||||
- client/transport
|
||||
- client/transport/cancellable
|
||||
- types/network
|
||||
- types/registry
|
||||
- types/time
|
||||
- types/blkiodev
|
||||
- name: github.com/docker/go-connections
|
||||
version: f549a9393d05688dff0992ef3efd8bbe6c628aeb
|
||||
subpackages:
|
||||
- nat
|
||||
- sockets
|
||||
- tlsconfig
|
||||
- name: github.com/docker/go-units
|
||||
version: 5d2041e26a699eaca682e2ea41c8f891e1060444
|
||||
- name: github.com/docker/libcompose
|
||||
version: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a
|
||||
- name: github.com/docker/libkv
|
||||
version: 3732f7ff1b56057c3158f10bceb1e79133025373
|
||||
subpackages:
|
||||
- store
|
||||
- store/boltdb
|
||||
- store/consul
|
||||
- store/etcd
|
||||
- store/zookeeper
|
||||
- name: github.com/docker/libtrust
|
||||
version: 9cbd2a1374f46905c68a4eb3694a130610adc62a
|
||||
- name: github.com/donovanhide/eventsource
|
||||
version: d8a3071799b98cacd30b6da92f536050ccfe6da4
|
||||
- name: github.com/elazarl/go-bindata-assetfs
|
||||
version: d5cac425555ca5cf00694df246e04f05e6a55150
|
||||
- name: github.com/flynn/go-shlex
|
||||
version: 3f9db97f856818214da2e1057f8ad84803971cff
|
||||
- name: github.com/gambol99/go-marathon
|
||||
version: ade11d1dc2884ee1f387078fc28509559b6235d1
|
||||
- name: github.com/go-check/check
|
||||
version: 11d3bc7aa68e238947792f30573146a3231fc0f1
|
||||
- name: github.com/golang/glog
|
||||
version: fca8c8854093a154ff1eb580aae10276ad6b1b5f
|
||||
- name: github.com/google/go-querystring
|
||||
version: 6bb77fe6f42b85397288d4f6f67ac72f8f400ee7
|
||||
subpackages:
|
||||
- query
|
||||
- name: github.com/gorilla/context
|
||||
version: 215affda49addc4c8ef7e2534915df2c8c35c6cd
|
||||
- name: github.com/gorilla/handlers
|
||||
version: 40694b40f4a928c062f56849989d3e9cd0570e5f
|
||||
- name: github.com/gorilla/mux
|
||||
version: f15e0c49460fd49eebe2bcc8486b05d1bef68d3a
|
||||
- name: github.com/gorilla/websocket
|
||||
version: e2e3d8414d0fbae04004f151979f4e27c6747fe7
|
||||
- name: github.com/hashicorp/consul
|
||||
version: de080672fee9e6104572eeea89eccdca135bb918
|
||||
subpackages:
|
||||
- api
|
||||
- name: github.com/hashicorp/hcl
|
||||
version: 2604f3bda7e8960c1be1063709e7d7f0765048d0
|
||||
subpackages:
|
||||
- hcl/ast
|
||||
- hcl/parser
|
||||
- hcl/token
|
||||
- json/parser
|
||||
- hcl/scanner
|
||||
- hcl/strconv
|
||||
- json/scanner
|
||||
- json/token
|
||||
- name: github.com/inconshreveable/mousetrap
|
||||
version: 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
|
||||
- name: github.com/kr/pretty
|
||||
version: add1dbc86daf0f983cd4a48ceb39deb95c729b67
|
||||
- name: github.com/kr/text
|
||||
version: bb797dc4fb8320488f47bf11de07a733d7233e1f
|
||||
- name: github.com/magiconair/properties
|
||||
version: c265cfa48dda6474e208715ca93e987829f572f8
|
||||
- name: github.com/mailgun/log
|
||||
version: 44874009257d4d47ba9806f1b7f72a32a015e4d8
|
||||
- name: github.com/mailgun/manners
|
||||
version: fada45142db3f93097ca917da107aa3fad0ffcb5
|
||||
- name: github.com/mailgun/multibuf
|
||||
version: 565402cd71fbd9c12aa7e295324ea357e970a61e
|
||||
- name: github.com/mailgun/timetools
|
||||
version: fd192d755b00c968d312d23f521eb0cdc6f66bd0
|
||||
- name: github.com/Microsoft/go-winio
|
||||
version: 9e2895e5f6c3f16473b91d37fae6e89990a4520c
|
||||
- name: github.com/miekg/dns
|
||||
version: 7e024ce8ce18b21b475ac6baf8fa3c42536bf2fa
|
||||
- name: github.com/mitchellh/mapstructure
|
||||
version: d2dd0262208475919e1a362f675cfc0e7c10e905
|
||||
- name: github.com/opencontainers/runc
|
||||
version: 4ab132458fc3e9dbeea624153e0331952dc4c8d5
|
||||
subpackages:
|
||||
- libcontainer/user
|
||||
- name: github.com/pmezard/go-difflib
|
||||
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
|
||||
subpackages:
|
||||
- difflib
|
||||
- name: github.com/samuel/go-zookeeper
|
||||
version: fa6674abf3f4580b946a01bf7a1ce4ba8766205b
|
||||
subpackages:
|
||||
- zk
|
||||
- name: github.com/Sirupsen/logrus
|
||||
version: 418b41d23a1bf978c06faea5313ba194650ac088
|
||||
- name: github.com/spf13/cast
|
||||
version: ee7b3e0353166ab1f3a605294ac8cd2b77953778
|
||||
- name: github.com/spf13/cobra
|
||||
version: 2ccf9e982a3e3eb21eba9c9ad8e546529fd74c71
|
||||
subpackages:
|
||||
- cobra
|
||||
- name: github.com/spf13/jwalterweatherman
|
||||
version: 33c24e77fb80341fe7130ee7c594256ff08ccc46
|
||||
- name: github.com/spf13/pflag
|
||||
version: 7f60f83a2c81bc3c3c0d5297f61ddfa68da9d3b7
|
||||
- name: github.com/spf13/viper
|
||||
version: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325
|
||||
- name: github.com/stretchr/objx
|
||||
version: cbeaeb16a013161a98496fad62933b1d21786672
|
||||
- name: github.com/stretchr/testify
|
||||
version: 6fe211e493929a8aac0469b93f28b1d0688a9a3a
|
||||
subpackages:
|
||||
- mock
|
||||
- assert
|
||||
- name: github.com/thoas/stats
|
||||
version: 54ed61c2b47e263ae2f01b86837b0c4bd1da28e8
|
||||
- name: github.com/unrolled/render
|
||||
version: 26b4e3aac686940fe29521545afad9966ddfc80c
|
||||
- name: github.com/vdemeester/docker-events
|
||||
version: bd72e1848b08db4b5ed1a2e9277621b9f5e5d1f3
|
||||
- name: github.com/vdemeester/libkermit
|
||||
version: 7e4e689a6fa9281e0fb9b7b9c297e22d5342a5ec
|
||||
- name: github.com/vdemeester/shakers
|
||||
version: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3
|
||||
- name: github.com/vulcand/oxy
|
||||
version: 8aaf36279137ac04ace3792a4f86098631b27d5a
|
||||
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/wendal/errors
|
||||
version: f66c77a7882b399795a8987ebf87ef64a427417e
|
||||
- name: github.com/xenolf/lego
|
||||
version: ca19a90028e242e878585941c2a27c8f3b3efc25
|
||||
subpackages:
|
||||
- acme
|
||||
- name: golang.org/x/crypto
|
||||
version: 9e7f5dc375abeb9619ea3c5c58502c428f457aa2
|
||||
subpackages:
|
||||
- ocsp
|
||||
- name: golang.org/x/net
|
||||
version: d9558e5c97f85372afee28cf2b6059d7d3818919
|
||||
subpackages:
|
||||
- context
|
||||
- publicsuffix
|
||||
- proxy
|
||||
- name: golang.org/x/sys
|
||||
version: eb2c74142fd19a79b3f237334c7384d5167b1b46
|
||||
subpackages:
|
||||
- unix
|
||||
- name: gopkg.in/alecthomas/kingpin.v2
|
||||
version: 639879d6110b1b0409410c7b737ef0bb18325038
|
||||
- name: gopkg.in/fsnotify.v1
|
||||
version: 96c060f6a6b7e0d6f75fddd10efeaca3e5d1bcb0
|
||||
- name: gopkg.in/mgo.v2
|
||||
version: 22287bab4379e1fbf6002fb4eb769888f3fb224c
|
||||
subpackages:
|
||||
- bson
|
||||
- name: gopkg.in/square/go-jose.v1
|
||||
version: 40d457b439244b546f023d056628e5184136899b
|
||||
subpackages:
|
||||
- cipher
|
||||
- json
|
||||
- name: gopkg.in/yaml.v2
|
||||
version: 7ad95dd0798a40da1ccdff6dff35fd177b5edf40
|
||||
devImports: []
|
||||
74
glide.yaml
@@ -4,12 +4,10 @@ import:
|
||||
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
|
||||
- package: github.com/containous/oxy
|
||||
ref: 0b5b371bce661385d35439204298fa6fb5db5463
|
||||
subpackages:
|
||||
- cbreaker
|
||||
- forward
|
||||
@@ -26,7 +24,7 @@ import:
|
||||
- zk
|
||||
- package: github.com/docker/libtrust
|
||||
ref: 9cbd2a1374f46905c68a4eb3694a130610adc62a
|
||||
- package: gopkg.in/check.v1
|
||||
- package: github.com/go-check/check
|
||||
ref: 11d3bc7aa68e238947792f30573146a3231fc0f1
|
||||
- package: golang.org/x/net
|
||||
ref: d9558e5c97f85372afee28cf2b6059d7d3818919
|
||||
@@ -39,25 +37,21 @@ import:
|
||||
- package: github.com/alecthomas/template
|
||||
ref: b867cc6ab45cece8143cfcc6fc9c77cf3f2c23c0
|
||||
- package: github.com/vdemeester/shakers
|
||||
ref: 8fe734f75f3a70b651cbfbf8a55a009da09e8dc5
|
||||
ref: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3
|
||||
- package: github.com/alecthomas/units
|
||||
ref: 6b4e7dc5e3143b85ea77909c72caf89416fc2915
|
||||
- package: github.com/gambol99/go-marathon
|
||||
ref: 0ba31bcb0d7633ba1888d744c42990eb15281cf1
|
||||
- package: github.com/mailgun/predicate
|
||||
ref: ade11d1dc2884ee1f387078fc28509559b6235d1
|
||||
- package: github.com/vulcand/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
|
||||
@@ -123,22 +117,60 @@ import:
|
||||
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
|
||||
ref: fada45142db3f93097ca917da107aa3fad0ffcb5
|
||||
- package: github.com/gorilla/context
|
||||
ref: 215affda49addc4c8ef7e2534915df2c8c35c6cd
|
||||
- package: github.com/codahale/hdrhistogram
|
||||
ref: 954f16e8b9ef0e5d5189456aa4c1202758e04f17
|
||||
- package: github.com/gorilla/websocket
|
||||
- package: github.com/donovanhide/eventsource
|
||||
ref: d8a3071799b98cacd30b6da92f536050ccfe6da4
|
||||
- package: github.com/golang/glog
|
||||
ref: fca8c8854093a154ff1eb580aae10276ad6b1b5f
|
||||
- package: github.com/spf13/cast
|
||||
ref: ee7b3e0353166ab1f3a605294ac8cd2b77953778
|
||||
- package: github.com/mitchellh/mapstructure
|
||||
- package: github.com/spf13/jwalterweatherman
|
||||
- package: github.com/spf13/pflag
|
||||
- package: github.com/wendal/errors
|
||||
- package: github.com/hashicorp/hcl
|
||||
- package: github.com/kr/pretty
|
||||
- package: github.com/magiconair/properties
|
||||
- package: github.com/kr/text
|
||||
- package: github.com/spf13/viper
|
||||
ref: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325
|
||||
- package: github.com/spf13/cobra
|
||||
subpackages:
|
||||
- /cobra
|
||||
- package: github.com/google/go-querystring/query
|
||||
- package: github.com/vulcand/vulcand/plugin/rewrite
|
||||
- package: github.com/stretchr/testify/mock
|
||||
- package: github.com/xenolf/lego
|
||||
- package: github.com/vdemeester/libkermit
|
||||
ref: 7e4e689a6fa9281e0fb9b7b9c297e22d5342a5ec
|
||||
- package: github.com/docker/libcompose
|
||||
version: e290a513ba909ca3afefd5cd611f3a3fe56f6a3a
|
||||
- package: github.com/docker/distribution
|
||||
version: ff6f38ccb69afa96214c7ee955359465d1fc767a
|
||||
subpackages:
|
||||
- reference
|
||||
- package: github.com/docker/engine-api
|
||||
subpackages:
|
||||
- client
|
||||
- types
|
||||
- types/container
|
||||
- types/filters
|
||||
- types/strslice
|
||||
- package: github.com/vdemeester/docker-events
|
||||
- package: github.com/docker/go-connections
|
||||
subpackages:
|
||||
- nat
|
||||
- sockets
|
||||
- tlsconfig
|
||||
- package: github.com/docker/go-units
|
||||
- package: github.com/mailgun/multibuf
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// SimpleSuite
|
||||
@@ -18,10 +19,10 @@ func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) {
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
c.Assert(err, checker.NotNil)
|
||||
c.Assert(string(output), checker.Contains, "Error reading file: open traefik.toml: no such file or directory")
|
||||
c.Assert(string(output), checker.Contains, "Error reading file: open : no such file or directory")
|
||||
|
||||
nonExistentFile := "non/existent/file.toml"
|
||||
cmd = exec.Command(traefikBinary, nonExistentFile)
|
||||
cmd = exec.Command(traefikBinary, "--configFile="+nonExistentFile)
|
||||
output, err = cmd.CombinedOutput()
|
||||
|
||||
c.Assert(err, checker.NotNil)
|
||||
@@ -29,30 +30,30 @@ func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) {
|
||||
}
|
||||
|
||||
func (s *SimpleSuite) TestInvalidConfigShouldFail(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "fixtures/invalid_configuration.toml")
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/invalid_configuration.toml")
|
||||
output, err := cmd.CombinedOutput()
|
||||
|
||||
c.Assert(err, checker.NotNil)
|
||||
c.Assert(string(output), checker.Contains, "Error reading file: Near line 1")
|
||||
c.Assert(string(output), checker.Contains, "Error reading file: While parsing config: Near line 1")
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
112
integration/consul_catalog_test.go
Normal file
@@ -0,0 +1,112 @@
|
||||
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) 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,
|
||||
},
|
||||
},
|
||||
&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)
|
||||
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,29 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
// Consul test suites (using libcompose)
|
||||
type ConsulSuite struct{ BaseSuite }
|
||||
|
||||
func (s *ConsulSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "consul")
|
||||
}
|
||||
|
||||
func (s *ConsulSuite) TestSimpleConfiguration(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "fixtures/consul/simple.toml")
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/consul/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 comfigure anything
|
||||
// Expected a 404 as we did not configure anything
|
||||
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/vdemeester/libkermit/docker"
|
||||
docker "github.com/vdemeester/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)
|
||||
|
||||
33
integration/etcd_test.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
)
|
||||
|
||||
// Etcd test suites (using libcompose)
|
||||
type EtcdSuite struct{ BaseSuite }
|
||||
|
||||
func (s *EtcdSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "etcd")
|
||||
}
|
||||
|
||||
func (s *EtcdSuite) TestSimpleConfiguration(c *check.C) {
|
||||
cmd := exec.Command(traefikBinary, "--configFile=fixtures/etcd/simple.toml")
|
||||
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)
|
||||
}
|
||||
@@ -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(500 * time.Millisecond)
|
||||
resp, err := http.Get("http://127.0.0.1/")
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
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(500 * time.Millisecond)
|
||||
resp, err := http.Get("http://127.0.0.1/")
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
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,11 +1,9 @@
|
||||
# Reverse proxy port
|
||||
#
|
||||
# Optional
|
||||
# Default: ":80"
|
||||
#
|
||||
# port = ":80"
|
||||
#
|
||||
# LogLevel
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[consul]
|
||||
|
||||
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,11 +1,9 @@
|
||||
# Reverse proxy port
|
||||
#
|
||||
# Optional
|
||||
# Default: ":80"
|
||||
#
|
||||
# port = ":80"
|
||||
#
|
||||
# LogLevel
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[docker]
|
||||
|
||||
10
integration/fixtures/etcd/simple.toml
Normal file
@@ -0,0 +1,10 @@
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[etcd]
|
||||
endpoint = "127.0.0.1:4003,127.0.0.1:4002,127.0.0.1:4001"
|
||||
@@ -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"
|
||||
|
||||
34
integration/fixtures/https/https_sni.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
logLevel = "DEBUG"
|
||||
|
||||
defaultEntryPoints = ["https"]
|
||||
|
||||
[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]
|
||||
|
||||
[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.servers.server1]
|
||||
url = "http://127.0.0.1:9010"
|
||||
[backends.backend2]
|
||||
[backends.backend2.servers.server1]
|
||||
url = "http://127.0.0.1:9020"
|
||||
|
||||
[frontends]
|
||||
[frontends.frontend1]
|
||||
backend = "backend1"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host:snitest.com"
|
||||
[frontends.frontend2]
|
||||
backend = "backend2"
|
||||
[frontends.frontend2.routes.test_2]
|
||||
rule = "Host:snitest.org"
|
||||
19
integration/fixtures/https/snitest.com.cert
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC/zCCAeegAwIBAgIJAL858pci5XyjMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV
|
||||
BAMMC3NuaXRlc3QuY29tMB4XDTE1MTEyMzIyMDU1NloXDTI1MTEyMDIyMDU1Nlow
|
||||
FjEUMBIGA1UEAwwLc25pdGVzdC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||
ggEKAoIBAQDVF25wEroSIUO/dNgHxlyt8pZFVpJ8fNaJw7cnlZ1JP2hLuEbmjAFT
|
||||
dHqS8wKDNYHktsBEOUfN/qbk0AiGb+SvhQw6kfM/QSj9fXVQ7KhYP9XYOekTOH7d
|
||||
M0Z2L3RGgqs8z+83exOOnAFVvIJCMZJXEeijV6iJlmpCcJa0Kg/JHlxhoWTEeZuU
|
||||
G+hITafk1yWOKorTCPlMhB30wuQoWfbHP+3G0bsERLXFiMANE8EtQu8+ZhfseBUh
|
||||
5Tu5gIC4Fnria7mRixAZeEiAblFP9h0vrNRcP3nmuVz0tHPIeQsJQiEhxaZ09oUW
|
||||
h9WqTsYCP1821+SVazM9oFRTpy6chZyTAgMBAAGjUDBOMB0GA1UdDgQWBBSz9mbX
|
||||
ia1TM5FG4Zgagaet24S8HDAfBgNVHSMEGDAWgBSz9mbXia1TM5FG4Zgagaet24S8
|
||||
HDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB79W8XTxlozh9w/W7T
|
||||
5vDev67G4T/wetABSb68CRrqojt78PMJuS89JarA8I3ts00O+0JYnsHxp+9qC7pf
|
||||
jWHcDSiLwRUMu7MXW/KIen1EB8BQNA0xWbAiQaWYPHzsBlX48+9wBe0HTDx7Lcxb
|
||||
OsmnXHBF5fd2EY+R8qJu+PyTDDL1WLItFJpzHiFiGiYF8Tyic3kkPjje6eIOxRmT
|
||||
hq+qbwApzbzz6h/VD5xR3zBDFBo2Xs5tdP264KIw/YXDpaXVBiJ5DDjQ3dtJw1G5
|
||||
yzgrHQZWJN8Gs8ZZgGdgRf7PHox8xEZtqPiMkChDz6T7Ha3U0xYN6TZGNZOR6DHs
|
||||
K9/8
|
||||
-----END CERTIFICATE-----
|
||||
27
integration/fixtures/https/snitest.com.key
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEA1RducBK6EiFDv3TYB8ZcrfKWRVaSfHzWicO3J5WdST9oS7hG
|
||||
5owBU3R6kvMCgzWB5LbARDlHzf6m5NAIhm/kr4UMOpHzP0Eo/X11UOyoWD/V2Dnp
|
||||
Ezh+3TNGdi90RoKrPM/vN3sTjpwBVbyCQjGSVxHoo1eoiZZqQnCWtCoPyR5cYaFk
|
||||
xHmblBvoSE2n5NcljiqK0wj5TIQd9MLkKFn2xz/txtG7BES1xYjADRPBLULvPmYX
|
||||
7HgVIeU7uYCAuBZ64mu5kYsQGXhIgG5RT/YdL6zUXD955rlc9LRzyHkLCUIhIcWm
|
||||
dPaFFofVqk7GAj9fNtfklWszPaBUU6cunIWckwIDAQABAoIBABAdQYDAKcoNMe5c
|
||||
i6mq2n9dBPghX9qCJkcswcEAk3BilySCvvnYRJFnEY3jSqFZfoUpPMjr+/4b78sF
|
||||
4F8qPwT27sHPH7H833ir8B86hlCGI0nCt1l4wD9CDWYKmKRsZT6oCtMLP6NdMMyn
|
||||
AMK4tPRYqlsP2fLtqQN1ODBPrfnraoNHtOVE784iBCD5dewICA5RIQG2i/d2+CGF
|
||||
+bahFqUXVCqHoxBz4AVvrRFL99VcP7P2iZyk6hDQ7fci7Xay8Wb/HutRxuqvF0aU
|
||||
bG6Enk6CCtNZHLwNPp4Hqft0Udvg2tG8okYwbEmoEO40nQsCSzRCpq5Uvzi+LX1k
|
||||
LykQ6+ECgYEA7x8vQoyOK60Q3LPpJFGDec2+XJPoesTfJTT6idaP7ukUL8p3FsUo
|
||||
9vtxRRfhSOdPoAINmrL0TyMekO2B6zXx0pmWVpMrFwZW6zMwZAnLp/w+3USpbGCy
|
||||
K12IIwvRYzTzKwoMTVAKTXm36b6oqr2La4bTdJR7REY6G374FrJb/H0CgYEA5CHk
|
||||
Ym0h7cf00fw9UEHRfzUZxmCfRWY6K8InOuHdLi+u4TiyXzs8x5s0e/DN/raNmTGx
|
||||
QO81UzuS3nKwc4n5QyXjVnhzR5DbbSACDwHtdnxZByL0D1KvPjtRF8F+rWXViXv2
|
||||
TM7UiOmn6R375FPSAPxeyMx8Womc3EnAAfLWGk8CgYEAv8I2WBv3dzcWqqbsdF+a
|
||||
G/fOjNdgO/PdLy1JLXiPfHwV4C1xSyVZMJd7wnjgBWLaC+sZldGk8kGrpXWSFlnw
|
||||
T38zfMIQcCp5Uax/RfpFA7XZhAAoDe2NdBFRtyknBXPU/dLVArsJSBAwWJa5FBNk
|
||||
1xoMQRVBtQLMXnh341utQNECgYEA4o1R2/ka16NaWmpPjXM/lD9skFgF84p4vFn8
|
||||
UXpaB3LtDdcbNH2Ed4mHToouWAR8jCUQLTcg0r53tRdaafMcKfXnVUka2nhdoHpH
|
||||
8RVt99u3IeIxU0I+q+OGPbw3jAV0UStcxpwj7q9zw4q2SuJ+y+HUUz7XQ6Yjs5Q9
|
||||
7PF2c/sCgYEAhdVn5gZ5FvYKrBi46t3pxPsWK476HmQEVHVi5+od7wg+araDelAe
|
||||
8QE8hc8qdZGbjdB/AHSPCeUxfO2vnpsCoSRs29o6pDvQuqvHYs+M53l5LEYeOjof
|
||||
t6J/DK5Pim2CAFjYFcZk8/Gyl5HjTw3PpdWxoPD5v2Xw3bbY57IIbm4=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
19
integration/fixtures/https/snitest.org.cert
Normal file
@@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC/zCCAeegAwIBAgIJALAYHG/vGqWEMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV
|
||||
BAMMC3NuaXRlc3Qub3JnMB4XDTE1MTEyMzIyMDU0NFoXDTI1MTEyMDIyMDU0NFow
|
||||
FjEUMBIGA1UEAwwLc25pdGVzdC5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
|
||||
ggEKAoIBAQC8b2Qv68Xnv4wgJ6HNupxABSUA5KXmv9g7pwwsFMSOK15o2qGFzx/x
|
||||
9loIi5pMIYIy4SVwJNrYUi772nCYMqSIVXlwct/CE70j2Jb2geIHu3jHbFWXruWb
|
||||
W1tGGUYzvnsOUziPE3rLWa/NObNYLLlUKJaxfHrxnpuKpQUsXzoLl25cJEVr4jg2
|
||||
ZITpdraxaBLisdlWY7EwwHBLu2nxH5Rn+nIjenFfdUwKF9s5dGy63tfBc8LX9yJk
|
||||
+kOwy1al/Wxa0DUb6rSt0QDCcD+rXnjk2zWPtsHz1btwtqM+FLtN5z0Lmnx7DF3C
|
||||
tCf1TMzduzZ6aeHk77zc664ZQun5cH33AgMBAAGjUDBOMB0GA1UdDgQWBBRn/nNz
|
||||
PUsmDKmKv3GGo3km5KKvUDAfBgNVHSMEGDAWgBRn/nNzPUsmDKmKv3GGo3km5KKv
|
||||
UDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBkuutIcbBdESgvNLLr
|
||||
k/8HUDuFm72lYHZFE+c76CxqYN52w02NCTiq1InoDUvqZXb/StATBwRRduTUPCj9
|
||||
KUkC7pOjAFxjzjExsHrtZSq01WinrxNI+qSKvI8jFngMHnwN1omTt7/D7nxeW5Of
|
||||
FJTkElnxtELAGHoIwZ+bKprnexefpn9UW84VJvJ2crSR63vBvdTrgsrEGW6kQj1I
|
||||
62laDpax4+x8t2h+sfG6uNIA1cFrG8Sk+O2Bi3ogB7Y/4e8r6WA23IRP+aSv0J2b
|
||||
k5fvuuXbIc979pQOoO03zG0S7Wpmpsw+9dQB9TOxGITOLfCZwEuIhnv+M9lLqCks
|
||||
7H2A
|
||||
-----END CERTIFICATE-----
|
||||
27
integration/fixtures/https/snitest.org.key
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEAvG9kL+vF57+MICehzbqcQAUlAOSl5r/YO6cMLBTEjiteaNqh
|
||||
hc8f8fZaCIuaTCGCMuElcCTa2FIu+9pwmDKkiFV5cHLfwhO9I9iW9oHiB7t4x2xV
|
||||
l67lm1tbRhlGM757DlM4jxN6y1mvzTmzWCy5VCiWsXx68Z6biqUFLF86C5duXCRF
|
||||
a+I4NmSE6Xa2sWgS4rHZVmOxMMBwS7tp8R+UZ/pyI3pxX3VMChfbOXRsut7XwXPC
|
||||
1/ciZPpDsMtWpf1sWtA1G+q0rdEAwnA/q1545Ns1j7bB89W7cLajPhS7Tec9C5p8
|
||||
ewxdwrQn9UzM3bs2emnh5O+83OuuGULp+XB99wIDAQABAoIBAGOn9bByXQQnhZAr
|
||||
5aLMIn6pOdyzEBptM4q42fMmOJ2HyjJiDjKaTCbHRu5mBoBk6FrIP+iDVUo6jKad
|
||||
7BZSEjoYGlWiKzyU+97NWWmdX1D/kOzHGq1RzhTPyAHWtA4Bm0sEMFFa2AJbuGIt
|
||||
NfBYFtuva6MKVmsamuBETewdoLEnxzzDFcuOaxXRfTC/ikWcYyB4KEWA5fjroUQC
|
||||
Llo9/UTGTkh1Hynv9AXY6Qia/RbrIQjKveKCRj6PjxyE/qN9qfmngczz2pK0hRhL
|
||||
Z+K06y8G+Yj1I1zm5jNg1kakVQKoBsnaYkmIUBUSmWv6ERotedOWtOAMlOKa+0l2
|
||||
DS7Ou2ECgYEA91doi+3XrMVsgyTEm/ArzEyRUfM5dCSvBCRFhO7QQp2OYAbjJk5S
|
||||
pmdpqmwTsXNNMU+XNkWCLug5pk0PTJwP0mVLE2fLYqCCXoyaMltQ0Yk2gaun/RwE
|
||||
w5EfyMwOQakLFY/ODvduQfyNpaoWgFz4/WPNTVNCGs04LepSGKaFNy0CgYEAwwgV
|
||||
jKeFA+QZGooTInyk07ZlAbenEPu/c2y3UUFxclP0CjP2/VBOpz9B62vhzCKbjD1c
|
||||
/L3x1CKC4n4lbeyHi4vrF69LX9SHr1Jm0SUtyKeV3g0EAzIWI0HFhVUkMvtbibQ4
|
||||
HXrLVCJO77xetQ7RQszss1z9g3WotAAiBMiQgDMCgYBTLjoilOIrYFmV4Q+dwa95
|
||||
DWbxwHJZ9NxG8EvQ4N95B7OR578Matqwy6ZlgeM9kiErrDCWN9oIHGEG5HN4uCM6
|
||||
BoaxB/8GNCSj13Uj6kHLtfF2ulvMa1fOzUd7J+TDgC4SGkKaFewmlOCuDf1zPdEe
|
||||
pimtD4rzqIB0MJFbaOT0IQKBgDBPjlb7IB3ooLdMQJUoXwP6iGa2gXHZioEjCv3b
|
||||
wihZ13e3i5UQEYuoRcH1RUd1wyYoBSKuQnsT2WwVZ1wlXSYaELAbQgaI9NtfBA0G
|
||||
sqKjsKICg13vSECPiEgQ4Rin3vLra4MR6c/7d6Y2+RbMhtWPQYrkm/+2Y4XDCqo4
|
||||
rGK1AoGAOFZ3RVhuwXzFdKNe32LM1wm1eZ7waxjI4bQS2xUN/3C/uWS7A3LaSlc3
|
||||
eRG3DaVpez4DQVupZDHMgxJUYqqKynUj6GD1YiaxGROj3TYCu6e7OxyhalhCllSu
|
||||
w/X5M802XqzLjeec5zHoZDfknnAkgR9MsxZYmZPFaDyL6GOKUB8=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -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"
|
||||
|
||||
111
integration/https_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
)
|
||||
|
||||
// HTTPSSuite
|
||||
type HTTPSSuite struct{ BaseSuite }
|
||||
|
||||
// TestWithSNIConfigHandshake involves a client sending a SNI hostname of
|
||||
// "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, "--configFile=fixtures/https/https_sni.toml")
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "snitest.com",
|
||||
}
|
||||
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()
|
||||
err = conn.Handshake()
|
||||
c.Assert(err, checker.IsNil, check.Commentf("TLS handshake error"))
|
||||
|
||||
cs := conn.ConnectionState()
|
||||
err = cs.PeerCertificates[0].VerifyHostname("snitest.com")
|
||||
c.Assert(err, checker.IsNil, check.Commentf("certificate did not match SNI servername"))
|
||||
}
|
||||
|
||||
// TestWithSNIConfigRoute involves a client sending HTTPS requests with
|
||||
// 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, "--configFile=fixtures/https/https_sni.toml")
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
backend1 := startTestServer("9010", 204)
|
||||
backend2 := startTestServer("9020", 205)
|
||||
defer backend1.Close()
|
||||
defer backend2.Close()
|
||||
|
||||
time.Sleep(2000 * time.Millisecond)
|
||||
|
||||
tr1 := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "snitest.com",
|
||||
},
|
||||
}
|
||||
tr2 := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "snitest.org",
|
||||
},
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: tr1}
|
||||
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", "*/*")
|
||||
resp, err := client.Do(req)
|
||||
c.Assert(err, checker.IsNil)
|
||||
// Expected a 204 (from backend1)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 204)
|
||||
|
||||
client = &http.Client{Transport: tr2}
|
||||
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", "*/*")
|
||||
resp, err = client.Do(req)
|
||||
c.Assert(err, checker.IsNil)
|
||||
// Expected a 205 (from backend2)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 205)
|
||||
}
|
||||
|
||||
func startTestServer(port string, statusCode int) (ts *httptest.Server) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(statusCode)
|
||||
})
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:"+port)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ts = &httptest.Server{
|
||||
Listener: listener,
|
||||
Config: &http.Server{Handler: handler},
|
||||
}
|
||||
ts.Start()
|
||||
return
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
compose "github.com/vdemeester/libkermit/compose/check"
|
||||
checker "github.com/vdemeester/shakers"
|
||||
check "gopkg.in/check.v1"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
@@ -24,97 +23,32 @@ func Test(t *testing.T) {
|
||||
|
||||
func init() {
|
||||
check.Suite(&SimpleSuite{})
|
||||
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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
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"
|
||||
30
integration/resources/compose/etcd.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
etcd1:
|
||||
image: quay.io/coreos/etcd:v2.2.0
|
||||
net: "host"
|
||||
command: >
|
||||
--name etcd1
|
||||
--listen-peer-urls http://localhost:7001
|
||||
--listen-client-urls http://localhost:4001
|
||||
--initial-advertise-peer-urls http://localhost:7001
|
||||
--advertise-client-urls http://localhost:4001
|
||||
--initial-cluster etcd1=http://localhost:7001,etcd2=http://localhost:7002,etcd3=http://localhost:7003
|
||||
etcd2:
|
||||
image: quay.io/coreos/etcd:v2.2.0
|
||||
net: "host"
|
||||
command: >
|
||||
--name etcd2
|
||||
--listen-peer-urls http://localhost:7002
|
||||
--listen-client-urls http://localhost:4002
|
||||
--initial-advertise-peer-urls http://localhost:7002
|
||||
--advertise-client-urls http://localhost:4002
|
||||
--initial-cluster etcd1=http://localhost:7001,etcd2=http://localhost:7002,etcd3=http://localhost:7003
|
||||
etcd3:
|
||||
image: quay.io/coreos/etcd:v2.2.0
|
||||
net: "host"
|
||||
command: >
|
||||
--name etcd3
|
||||
--listen-peer-urls http://localhost:7003
|
||||
--listen-client-urls http://localhost:4003
|
||||
--initial-advertise-peer-urls http://localhost:7003
|
||||
--advertise-client-urls http://localhost:4003
|
||||
--initial-cluster etcd1=http://localhost:7001,etcd2=http://localhost:7002,etcd3=http://localhost:7003
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
40
middlewares/handlerSwitcher.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/gorilla/mux"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// HandlerSwitcher allows hot switching of http.ServeMux
|
||||
type HandlerSwitcher struct {
|
||||
handler *mux.Router
|
||||
handlerLock *sync.Mutex
|
||||
}
|
||||
|
||||
// NewHandlerSwitcher builds a new instance of HandlerSwitcher
|
||||
func NewHandlerSwitcher(newHandler *mux.Router) (hs *HandlerSwitcher) {
|
||||
return &HandlerSwitcher{
|
||||
handler: newHandler,
|
||||
handlerLock: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *HandlerSwitcher) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
hs.handlerLock.Lock()
|
||||
handlerBackup := hs.handler
|
||||
hs.handlerLock.Unlock()
|
||||
handlerBackup.ServeHTTP(rw, r)
|
||||
}
|
||||
|
||||
// GetHandler returns the current http.ServeMux
|
||||
func (hs *HandlerSwitcher) GetHandler() (newHandler *mux.Router) {
|
||||
return hs.handler
|
||||
}
|
||||
|
||||
// UpdateHandler safely updates the current http.ServeMux with a new one
|
||||
func (hs *HandlerSwitcher) UpdateHandler(newHandler *mux.Router) {
|
||||
hs.handlerLock.Lock()
|
||||
hs.handler = newHandler
|
||||
defer hs.handlerLock.Unlock()
|
||||
}
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
49
mkdocs.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
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
|
||||
- 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,19 +1,20 @@
|
||||
package provider
|
||||
|
||||
import "github.com/emilevauge/traefik/types"
|
||||
import (
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/boltdb"
|
||||
)
|
||||
|
||||
// BoltDb holds configurations of the BoltDb provider.
|
||||
type BoltDb struct {
|
||||
Watch bool
|
||||
Endpoint string
|
||||
Prefix string
|
||||
Filename string
|
||||
KvProvider *Kv
|
||||
Kv `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *BoltDb) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
provider.KvProvider = NewBoltDbProvider(provider)
|
||||
return provider.KvProvider.provide(configurationChan)
|
||||
provider.storeType = store.BOLTDB
|
||||
boltdb.Register()
|
||||
return provider.provide(configurationChan)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
package provider
|
||||
|
||||
import "github.com/emilevauge/traefik/types"
|
||||
import (
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/consul"
|
||||
)
|
||||
|
||||
// Consul holds configurations of the Consul provider.
|
||||
type Consul struct {
|
||||
Watch bool
|
||||
Endpoint string
|
||||
Prefix string
|
||||
Filename string
|
||||
KvProvider *Kv
|
||||
Kv `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Consul) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
provider.KvProvider = NewConsulProvider(provider)
|
||||
return provider.KvProvider.provide(configurationChan)
|
||||
provider.storeType = store.CONSUL
|
||||
consul.Register()
|
||||
return provider.provide(configurationChan)
|
||||
}
|
||||
|
||||
200
provider/consul_catalog.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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
|
||||
)
|
||||
|
||||
// ConsulCatalog holds configurations of the Consul catalog provider.
|
||||
type ConsulCatalog struct {
|
||||
BaseProvider `mapstructure:",squash"`
|
||||
Endpoint string
|
||||
Domain string
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
type catalogUpdate struct {
|
||||
Service string
|
||||
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
|
||||
}
|
||||
|
||||
return catalogUpdate{
|
||||
Service: service,
|
||||
Nodes: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) getBackend(node *api.ServiceEntry) string {
|
||||
return strings.ToLower(node.Service.Service)
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) getFrontendValue(service string) string {
|
||||
return "Host:" + service + "." + provider.Domain
|
||||
}
|
||||
|
||||
func (provider *ConsulCatalog) buildConfig(catalog []catalogUpdate) *types.Configuration {
|
||||
var FuncMap = template.FuncMap{
|
||||
"getBackend": provider.getBackend,
|
||||
"getFrontendValue": provider.getFrontendValue,
|
||||
"replace": replace,
|
||||
}
|
||||
|
||||
allNodes := []*api.ServiceEntry{}
|
||||
serviceNames := []string{}
|
||||
for _, info := range catalog {
|
||||
if len(info.Nodes) > 0 {
|
||||
serviceNames = append(serviceNames, info.Service)
|
||||
allNodes = append(allNodes, info.Nodes...)
|
||||
}
|
||||
}
|
||||
|
||||
templateObjects := struct {
|
||||
Services []string
|
||||
Nodes []*api.ServiceEntry
|
||||
}{
|
||||
Services: serviceNames,
|
||||
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) error {
|
||||
stopCh := make(chan struct{})
|
||||
serviceCatalog := provider.watchServices(stopCh)
|
||||
|
||||
defer close(stopCh)
|
||||
|
||||
for {
|
||||
select {
|
||||
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) error {
|
||||
config := api.DefaultConfig()
|
||||
config.Address = provider.Endpoint
|
||||
client, err := api.NewClient(config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provider.client = client
|
||||
|
||||
safe.Go(func() {
|
||||
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)
|
||||
}
|
||||
err := backoff.RetryNotify(worker, backoff.NewExponentialBackOff(), notify)
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot connect to consul server %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
110
provider/consul_catalog_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
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 string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
service: "foo",
|
||||
expected: "Host:foo.localhost",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
actual := provider.getFrontendValue(e.service)
|
||||
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: "test",
|
||||
},
|
||||
},
|
||||
expectedFrontends: map[string]*types.Frontend{},
|
||||
expectedBackends: map[string]*types.Backend{},
|
||||
},
|
||||
{
|
||||
nodes: []catalogUpdate{
|
||||
{
|
||||
Service: "test",
|
||||
Nodes: []*api.ServiceEntry{
|
||||
{
|
||||
Service: &api.AgentService{
|
||||
Service: "test",
|
||||
Address: "127.0.0.1",
|
||||
Port: 80,
|
||||
},
|
||||
Node: &api.Node{
|
||||
Node: "localhost",
|
||||
Address: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedFrontends: map[string]*types.Frontend{
|
||||
"frontend-test": {
|
||||
Backend: "backend-test",
|
||||
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": {
|
||||
URL: "http://127.0.0.1:80",
|
||||
},
|
||||
},
|
||||
CircuitBreaker: nil,
|
||||
LoadBalancer: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,225 +1,284 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/cenkalti/backoff"
|
||||
"github.com/emilevauge/traefik/autogen"
|
||||
"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 {
|
||||
Watch bool
|
||||
Endpoint string
|
||||
Filename string
|
||||
Domain string
|
||||
BaseProvider `mapstructure:",squash"`
|
||||
Endpoint string
|
||||
Domain string
|
||||
TLS *DockerTLS
|
||||
}
|
||||
|
||||
// DockerTLS holds TLS specific configurations
|
||||
type DockerTLS struct {
|
||||
CA string
|
||||
Cert string
|
||||
Key string
|
||||
InsecureSkipVerify bool
|
||||
}
|
||||
|
||||
func (provider *Docker) createClient() (client.APIClient, error) {
|
||||
var httpClient *http.Client
|
||||
httpHeaders := map[string]string{
|
||||
// FIXME(vdemeester) use version here O:)
|
||||
"User-Agent": "Traefik",
|
||||
}
|
||||
if provider.TLS != nil {
|
||||
tlsOptions := tlsconfig.Options{
|
||||
CAFile: provider.TLS.CA,
|
||||
CertFile: provider.TLS.Cert,
|
||||
KeyFile: provider.TLS.Key,
|
||||
InsecureSkipVerify: provider.TLS.InsecureSkipVerify,
|
||||
}
|
||||
config, err := tlsconfig.Client(tlsOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: config,
|
||||
}
|
||||
proto, addr, _, err := client.ParseHost(provider.Endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sockets.ConfigureTransport(tr, proto, addr)
|
||||
|
||||
httpClient = &http.Client{
|
||||
Transport: tr,
|
||||
}
|
||||
}
|
||||
return client.NewClient(provider.Endpoint, DockerAPIVersion, httpClient, httpHeaders)
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
safe.Go(func() {
|
||||
operation := func() error {
|
||||
var err error
|
||||
|
||||
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(dockerClient)
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{"docker", configuration}
|
||||
configuration := provider.loadDockerConfig(containers)
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "docker",
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
eventHandler.Handle("start", startStopHandle)
|
||||
eventHandler.Handle("die", startStopHandle)
|
||||
errChan := events.MonitorWithHandler(ctx, dockerClient, options, eventHandler)
|
||||
if err := <-errChan; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
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(dockerClient)
|
||||
configurationChan <- types.ConfigMessage{"docker", configuration}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Docker) loadDockerConfig(dockerClient *docker.Client) *types.Configuration {
|
||||
func (provider *Docker) loadDockerConfig(containersInspected []dockertypes.ContainerJSON) *types.Configuration {
|
||||
var DockerFuncMap = template.FuncMap{
|
||||
"getBackend": func(container docker.Container) string {
|
||||
if label, err := provider.getLabel(container, "traefik.backend"); err == nil {
|
||||
return label
|
||||
}
|
||||
return provider.getEscapedName(container.Name)
|
||||
},
|
||||
"getPort": func(container docker.Container) string {
|
||||
if label, err := provider.getLabel(container, "traefik.port"); err == nil {
|
||||
return label
|
||||
}
|
||||
for key := range container.NetworkSettings.Ports {
|
||||
return key.Port()
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"getWeight": func(container docker.Container) string {
|
||||
if label, err := provider.getLabel(container, "traefik.weight"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "0"
|
||||
},
|
||||
"getDomain": func(container docker.Container) string {
|
||||
if label, err := provider.getLabel(container, "traefik.domain"); err == nil {
|
||||
return label
|
||||
}
|
||||
return provider.Domain
|
||||
},
|
||||
"getProtocol": func(container docker.Container) string {
|
||||
if label, err := provider.getLabel(container, "traefik.protocol"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "http"
|
||||
},
|
||||
"getPassHostHeader": func(container docker.Container) string {
|
||||
if passHostHeader, err := provider.getLabel(container, "traefik.frontend.passHostHeader"); err == nil {
|
||||
return passHostHeader
|
||||
}
|
||||
return "false"
|
||||
},
|
||||
"getFrontendValue": provider.GetFrontendValue,
|
||||
"getFrontendRule": provider.GetFrontendRule,
|
||||
"replace": func(s1 string, s2 string, s3 string) string {
|
||||
return strings.Replace(s3, s1, s2, -1)
|
||||
},
|
||||
}
|
||||
configuration := new(types.Configuration)
|
||||
containerList, _ := dockerClient.ListContainers(docker.ListContainersOptions{})
|
||||
containersInspected := []docker.Container{}
|
||||
frontends := map[string][]docker.Container{}
|
||||
|
||||
// get inspect containers
|
||||
for _, container := range containerList {
|
||||
containerInspected, _ := dockerClient.InspectContainer(container.ID)
|
||||
containersInspected = append(containersInspected, *containerInspected)
|
||||
"getBackend": provider.getBackend,
|
||||
"getPort": provider.getPort,
|
||||
"getWeight": provider.getWeight,
|
||||
"getDomain": provider.getDomain,
|
||||
"getProtocol": provider.getProtocol,
|
||||
"getPassHostHeader": provider.getPassHostHeader,
|
||||
"getEntryPoints": provider.getEntryPoints,
|
||||
"getFrontendRule": provider.getFrontendRule,
|
||||
"replace": replace,
|
||||
}
|
||||
|
||||
// filter containers
|
||||
filteredContainers := fun.Filter(func(container docker.Container) bool {
|
||||
if len(container.NetworkSettings.Ports) == 0 {
|
||||
log.Debugf("Filtering container without port %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
|
||||
}
|
||||
if container.Config.Labels["traefik.enable"] == "false" {
|
||||
log.Debugf("Filtering disabled container %s", container.Name)
|
||||
return false
|
||||
}
|
||||
|
||||
labels, err := provider.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
|
||||
}, containersInspected).([]docker.Container)
|
||||
filteredContainers := fun.Filter(containerFilter, containersInspected).([]dockertypes.ContainerJSON)
|
||||
|
||||
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,
|
||||
frontends,
|
||||
provider.Domain,
|
||||
}
|
||||
tmpl := template.New(provider.Filename).Funcs(DockerFuncMap)
|
||||
if len(provider.Filename) > 0 {
|
||||
_, err := tmpl.ParseFiles(provider.Filename)
|
||||
if err != nil {
|
||||
log.Error("Error reading file", err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
buf, err := autogen.Asset("templates/docker.tmpl")
|
||||
if err != nil {
|
||||
log.Error("Error reading file", err)
|
||||
}
|
||||
_, err = tmpl.Parse(string(buf))
|
||||
if err != nil {
|
||||
log.Error("Error reading file", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
err := tmpl.Execute(&buffer, templateObjects)
|
||||
configuration, err := provider.getConfiguration("templates/docker.tmpl", DockerFuncMap, templateObjects)
|
||||
if err != nil {
|
||||
log.Error("Error with docker template", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := toml.Decode(buffer.String(), configuration); err != nil {
|
||||
log.Error("Error creating docker configuration ", err)
|
||||
return nil
|
||||
log.Error(err)
|
||||
}
|
||||
return configuration
|
||||
}
|
||||
|
||||
func (provider *Docker) getFrontendName(container docker.Container) string {
|
||||
func containerFilter(container dockertypes.ContainerJSON) bool {
|
||||
if len(container.NetworkSettings.Ports) == 0 {
|
||||
log.Debugf("Filtering container without port %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
|
||||
}
|
||||
|
||||
if container.Config.Labels["traefik.enable"] == "false" {
|
||||
log.Debugf("Filtering disabled container %s", container.Name)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
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)
|
||||
return normalize(provider.getFrontendRule(container))
|
||||
}
|
||||
|
||||
func (provider *Docker) getEscapedName(name string) string {
|
||||
return strings.Replace(name, "/", "", -1)
|
||||
// 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 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:" + getEscapedName(container.Name) + "." + provider.Domain
|
||||
}
|
||||
|
||||
func (provider *Docker) getLabel(container docker.Container, label string) (string, error) {
|
||||
func (provider *Docker) getBackend(container dockertypes.ContainerJSON) string {
|
||||
if label, err := getLabel(container, "traefik.backend"); err == nil {
|
||||
return label
|
||||
}
|
||||
return normalize(container.Name)
|
||||
}
|
||||
|
||||
func (provider *Docker) getPort(container dockertypes.ContainerJSON) string {
|
||||
if label, err := getLabel(container, "traefik.port"); err == nil {
|
||||
return label
|
||||
}
|
||||
for key := range container.NetworkSettings.Ports {
|
||||
return key.Port()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (provider *Docker) getWeight(container dockertypes.ContainerJSON) string {
|
||||
if label, err := getLabel(container, "traefik.weight"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "1"
|
||||
}
|
||||
|
||||
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 dockertypes.ContainerJSON) string {
|
||||
if label, err := getLabel(container, "traefik.protocol"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (provider *Docker) getPassHostHeader(container dockertypes.ContainerJSON) string {
|
||||
if passHostHeader, err := getLabel(container, "traefik.frontend.passHostHeader"); err == nil {
|
||||
return passHostHeader
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func (provider *Docker) getEntryPoints(container 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
|
||||
@@ -228,11 +287,11 @@ func (provider *Docker) getLabel(container docker.Container, label string) (stri
|
||||
return "", errors.New("Label not found:" + label)
|
||||
}
|
||||
|
||||
func (provider *Docker) 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 {
|
||||
foundLabel, err := provider.getLabel(container, label)
|
||||
foundLabel, err := getLabel(container, label)
|
||||
// Error out only if one of them is defined.
|
||||
if err != nil {
|
||||
globalErr = errors.New("Label not found: " + label)
|
||||
@@ -244,20 +303,20 @@ func (provider *Docker) getLabels(container docker.Container, labels []string) (
|
||||
return foundLabels, globalErr
|
||||
}
|
||||
|
||||
// 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 := provider.getLabel(container, "traefik.frontend.value"); err == nil {
|
||||
return label
|
||||
func listContainers(dockerClient client.APIClient) ([]dockertypes.ContainerJSON, error) {
|
||||
containerList, err := dockerClient.ContainerList(context.Background(), dockertypes.ContainerListOptions{})
|
||||
if err != nil {
|
||||
return []dockertypes.ContainerJSON{}, err
|
||||
}
|
||||
return provider.getEscapedName(container.Name) + "." + provider.Domain
|
||||
}
|
||||
containersInspected := []dockertypes.ContainerJSON{}
|
||||
|
||||
// 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 {
|
||||
if label, err := provider.getLabel(container, "traefik.frontend.rule"); err == nil {
|
||||
return label
|
||||
// get inspect containers
|
||||
for _, container := range containerList {
|
||||
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 "Host"
|
||||
return containersInspected, nil
|
||||
}
|
||||
|
||||
870
provider/docker_test.go
Normal file
@@ -0,0 +1,870 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containous/traefik/types"
|
||||
docker "github.com/docker/engine-api/types"
|
||||
"github.com/docker/engine-api/types/container"
|
||||
"github.com/docker/engine-api/types/network"
|
||||
"github.com/docker/go-connections/nat"
|
||||
)
|
||||
|
||||
func TestDockerGetFrontendName(t *testing.T) {
|
||||
provider := &Docker{
|
||||
Domain: "docker.localhost",
|
||||
}
|
||||
|
||||
containers := []struct {
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "Host-foo-docker-localhost",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "bar",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Headers:User-Agent,bat/0.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "Headers-User-Agent-bat-0-1-0",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "Host-foo-bar",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Path:/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "Path-test",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "PathPrefix:/test2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "PathPrefix-test2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
actual := provider.getFrontendName(e.container)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerGetFrontendRule(t *testing.T) {
|
||||
provider := &Docker{
|
||||
Domain: "docker.localhost",
|
||||
}
|
||||
|
||||
containers := []struct {
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "Host:foo.docker.localhost",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "bar",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "Host:bar.docker.localhost",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "Host:foo.bar",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Path:/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "Path:/test",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
actual := provider.getFrontendRule(e.container)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerGetBackend(t *testing.T) {
|
||||
provider := &Docker{}
|
||||
|
||||
containers := []struct {
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "foo",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "bar",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "bar",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.backend": "foobar",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "foobar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
actual := provider.getBackend(e.container)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerGetPort(t *testing.T) {
|
||||
provider := &Docker{}
|
||||
|
||||
containers := []struct {
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "bar",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "80",
|
||||
},
|
||||
// FIXME handle this better..
|
||||
// {
|
||||
// container: docker.ContainerJSON{
|
||||
// Name: "bar",
|
||||
// Config: &container.Config{},
|
||||
// NetworkSettings: &docker.NetworkSettings{
|
||||
// Ports: map[docker.Port][]docker.PortBinding{
|
||||
// "80/tcp": []docker.PortBinding{},
|
||||
// "443/tcp": []docker.PortBinding{},
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// expected: "80",
|
||||
// },
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.port": "8080",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "8080",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
actual := provider.getPort(e.container)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerGetWeight(t *testing.T) {
|
||||
provider := &Docker{}
|
||||
|
||||
containers := []struct {
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "1",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.weight": "10",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "10",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
actual := provider.getWeight(e.container)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerGetDomain(t *testing.T) {
|
||||
provider := &Docker{
|
||||
Domain: "docker.localhost",
|
||||
}
|
||||
|
||||
containers := []struct {
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "docker.localhost",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.domain": "foo.bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "foo.bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
actual := provider.getDomain(e.container)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerGetProtocol(t *testing.T) {
|
||||
provider := &Docker{}
|
||||
|
||||
containers := []struct {
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "http",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.protocol": "https",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "https",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
actual := provider.getProtocol(e.container)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerGetPassHostHeader(t *testing.T) {
|
||||
provider := &Docker{}
|
||||
|
||||
containers := []struct {
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "foo",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "false",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.passHostHeader": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "true",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
actual := provider.getPassHostHeader(e.container)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerGetLabel(t *testing.T) {
|
||||
containers := []struct {
|
||||
container docker.ContainerJSON
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expected: "Label not found:",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
label, err := getLabel(e.container, "foo")
|
||||
if e.expected != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), e.expected) {
|
||||
t.Fatalf("expected an error with %q, got %v", e.expected, err)
|
||||
}
|
||||
} else {
|
||||
if label != "bar" {
|
||||
t.Fatalf("expected label 'bar', got %s", label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerGetLabels(t *testing.T) {
|
||||
containers := []struct {
|
||||
container docker.ContainerJSON
|
||||
expectedLabels map[string]string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
Config: &container.Config{},
|
||||
},
|
||||
expectedLabels: map[string]string{},
|
||||
expectedError: "Label not found:",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"foo": "fooz",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedLabels: map[string]string{
|
||||
"foo": "fooz",
|
||||
},
|
||||
expectedError: "Label not found: bar",
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"foo": "fooz",
|
||||
"bar": "barz",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedLabels: map[string]string{
|
||||
"foo": "fooz",
|
||||
"bar": "barz",
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
labels, err := getLabels(e.container, []string{"foo", "bar"})
|
||||
if !reflect.DeepEqual(labels, e.expectedLabels) {
|
||||
t.Fatalf("expect %v, got %v", e.expectedLabels, labels)
|
||||
}
|
||||
if e.expectedError != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), e.expectedError) {
|
||||
t.Fatalf("expected an error with %q, got %v", e.expectedError, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerTraefikFilter(t *testing.T) {
|
||||
containers := []struct {
|
||||
container docker.ContainerJSON
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.enable": "false",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
"443/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.port": "80",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
"443/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.enable": "true",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.enable": "anything",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
container: docker.ContainerJSON{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "container",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range containers {
|
||||
actual := containerFilter(e.container)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %v for %+v, got %+v", e.expected, e, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDockerLoadDockerConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
containers []docker.ContainerJSON
|
||||
expectedFrontends map[string]*types.Frontend
|
||||
expectedBackends map[string]*types.Backend
|
||||
}{
|
||||
{
|
||||
containers: []docker.ContainerJSON{},
|
||||
expectedFrontends: map[string]*types.Frontend{},
|
||||
expectedBackends: map[string]*types.Backend{},
|
||||
},
|
||||
{
|
||||
containers: []docker.ContainerJSON{
|
||||
{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test",
|
||||
},
|
||||
Config: &container.Config{},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"bridge": {
|
||||
IPAddress: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedFrontends: map[string]*types.Frontend{
|
||||
`"frontend-Host-test-docker-localhost"`: {
|
||||
Backend: "backend-test",
|
||||
EntryPoints: []string{},
|
||||
Routes: map[string]types.Route{
|
||||
`"route-frontend-Host-test-docker-localhost"`: {
|
||||
Rule: "Host:test.docker.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedBackends: map[string]*types.Backend{
|
||||
"backend-test": {
|
||||
Servers: map[string]types.Server{
|
||||
"server-test": {
|
||||
URL: "http://127.0.0.1:80",
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
CircuitBreaker: nil,
|
||||
LoadBalancer: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
containers: []docker.ContainerJSON{
|
||||
{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test1",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.backend": "foobar",
|
||||
"traefik.frontend.entryPoints": "http,https",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"bridge": {
|
||||
IPAddress: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ContainerJSONBase: &docker.ContainerJSONBase{
|
||||
Name: "test2",
|
||||
},
|
||||
Config: &container.Config{
|
||||
Labels: map[string]string{
|
||||
"traefik.backend": "foobar",
|
||||
},
|
||||
},
|
||||
NetworkSettings: &docker.NetworkSettings{
|
||||
NetworkSettingsBase: docker.NetworkSettingsBase{
|
||||
Ports: nat.PortMap{
|
||||
"80/tcp": {},
|
||||
},
|
||||
},
|
||||
Networks: map[string]*network.EndpointSettings{
|
||||
"bridge": {
|
||||
IPAddress: "127.0.0.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedFrontends: map[string]*types.Frontend{
|
||||
`"frontend-Host-test1-docker-localhost"`: {
|
||||
Backend: "backend-foobar",
|
||||
EntryPoints: []string{"http", "https"},
|
||||
Routes: map[string]types.Route{
|
||||
`"route-frontend-Host-test1-docker-localhost"`: {
|
||||
Rule: "Host:test1.docker.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
`"frontend-Host-test2-docker-localhost"`: {
|
||||
Backend: "backend-foobar",
|
||||
EntryPoints: []string{},
|
||||
Routes: map[string]types.Route{
|
||||
`"route-frontend-Host-test2-docker-localhost"`: {
|
||||
Rule: "Host:test2.docker.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedBackends: map[string]*types.Backend{
|
||||
"backend-foobar": {
|
||||
Servers: map[string]types.Server{
|
||||
"server-test1": {
|
||||
URL: "http://127.0.0.1:80",
|
||||
Weight: 1,
|
||||
},
|
||||
"server-test2": {
|
||||
URL: "http://127.0.0.1:80",
|
||||
Weight: 1,
|
||||
},
|
||||
},
|
||||
CircuitBreaker: nil,
|
||||
LoadBalancer: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider := &Docker{
|
||||
Domain: "docker.localhost",
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actualConfig := provider.loadDockerConfig(c.containers)
|
||||
// Compare backends
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
package provider
|
||||
|
||||
import "github.com/emilevauge/traefik/types"
|
||||
import (
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/etcd"
|
||||
)
|
||||
|
||||
// Etcd holds configurations of the Etcd provider.
|
||||
type Etcd struct {
|
||||
Watch bool
|
||||
Endpoint string
|
||||
Prefix string
|
||||
Filename string
|
||||
KvProvider *Kv
|
||||
Kv `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Etcd) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
provider.KvProvider = NewEtcdProvider(provider)
|
||||
return provider.KvProvider.provide(configurationChan)
|
||||
provider.storeType = store.ETCD
|
||||
etcd.Register()
|
||||
return provider.provide(configurationChan)
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/emilevauge/traefik/types"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"gopkg.in/fsnotify.v1"
|
||||
)
|
||||
|
||||
// File holds configurations of the File provider.
|
||||
type File struct {
|
||||
Watch bool
|
||||
Filename string
|
||||
BaseProvider `mapstructure:",squash"`
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
@@ -35,7 +35,7 @@ func (provider *File) Provide(configurationChan chan<- types.ConfigMessage) erro
|
||||
|
||||
if provider.Watch {
|
||||
// Process events
|
||||
go func() {
|
||||
safe.Go(func() {
|
||||
defer watcher.Close()
|
||||
for {
|
||||
select {
|
||||
@@ -44,14 +44,17 @@ func (provider *File) Provide(configurationChan chan<- types.ConfigMessage) erro
|
||||
log.Debug("File event:", event)
|
||||
configuration := provider.loadFileConfig(file.Name())
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{"file", configuration}
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "file",
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
case error := <-watcher.Errors:
|
||||
log.Error("Watcher event error", error)
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
err = watcher.Add(filepath.Dir(file.Name()))
|
||||
if err != nil {
|
||||
log.Error("Error adding file watcher", err)
|
||||
@@ -60,7 +63,10 @@ func (provider *File) Provide(configurationChan chan<- types.ConfigMessage) erro
|
||||
}
|
||||
|
||||
configuration := provider.loadFileConfig(file.Name())
|
||||
configurationChan <- types.ConfigMessage{"file", configuration}
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "file",
|
||||
Configuration: configuration,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
279
provider/kv.go
@@ -2,99 +2,97 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/boltdb"
|
||||
"github.com/docker/libkv/store/consul"
|
||||
"github.com/docker/libkv/store/etcd"
|
||||
"github.com/docker/libkv/store/zookeeper"
|
||||
"github.com/emilevauge/traefik/autogen"
|
||||
"github.com/emilevauge/traefik/types"
|
||||
)
|
||||
|
||||
// Kv holds common configurations of key-value providers.
|
||||
type Kv struct {
|
||||
Watch bool
|
||||
Endpoint string
|
||||
Prefix string
|
||||
Filename string
|
||||
StoreType store.Backend
|
||||
kvclient store.Store
|
||||
BaseProvider `mapstructure:",squash"`
|
||||
Endpoint string
|
||||
Prefix string
|
||||
TLS *KvTLS
|
||||
storeType store.Backend
|
||||
kvclient store.Store
|
||||
}
|
||||
|
||||
// NewConsulProvider returns a Consul provider.
|
||||
func NewConsulProvider(provider *Consul) *Kv {
|
||||
kvProvider := new(Kv)
|
||||
kvProvider.Watch = provider.Watch
|
||||
kvProvider.Endpoint = provider.Endpoint
|
||||
kvProvider.Prefix = provider.Prefix
|
||||
kvProvider.Filename = provider.Filename
|
||||
kvProvider.StoreType = store.CONSUL
|
||||
return kvProvider
|
||||
// KvTLS holds TLS specific configurations
|
||||
type KvTLS struct {
|
||||
CA string
|
||||
Cert string
|
||||
Key string
|
||||
InsecureSkipVerify bool
|
||||
}
|
||||
|
||||
// NewEtcdProvider returns a Etcd provider.
|
||||
func NewEtcdProvider(provider *Etcd) *Kv {
|
||||
kvProvider := new(Kv)
|
||||
kvProvider.Watch = provider.Watch
|
||||
kvProvider.Endpoint = provider.Endpoint
|
||||
kvProvider.Prefix = provider.Prefix
|
||||
kvProvider.Filename = provider.Filename
|
||||
kvProvider.StoreType = store.ETCD
|
||||
return kvProvider
|
||||
}
|
||||
func (provider *Kv) watchKv(configurationChan chan<- types.ConfigMessage, prefix string) {
|
||||
for {
|
||||
chanKeys, err := provider.kvclient.WatchTree(provider.Prefix, make(chan struct{}) /* stop chan */)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to WatchTree %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// NewZkProvider returns a Zookepper provider.
|
||||
func NewZkProvider(provider *Zookepper) *Kv {
|
||||
kvProvider := new(Kv)
|
||||
kvProvider.Watch = provider.Watch
|
||||
kvProvider.Endpoint = provider.Endpoint
|
||||
kvProvider.Prefix = provider.Prefix
|
||||
kvProvider.Filename = provider.Filename
|
||||
kvProvider.StoreType = store.ZK
|
||||
return kvProvider
|
||||
}
|
||||
|
||||
// NewBoltDbProvider returns a BoldDb provider.
|
||||
func NewBoltDbProvider(provider *BoltDb) *Kv {
|
||||
kvProvider := new(Kv)
|
||||
kvProvider.Watch = provider.Watch
|
||||
kvProvider.Endpoint = provider.Endpoint
|
||||
kvProvider.Prefix = provider.Prefix
|
||||
kvProvider.Filename = provider.Filename
|
||||
kvProvider.StoreType = store.BOLTDB
|
||||
return kvProvider
|
||||
for range chanKeys {
|
||||
configuration := provider.loadConfig()
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: string(provider.storeType),
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Warnf("Intermittent failure to WatchTree KV. Retrying.")
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
switch provider.StoreType {
|
||||
case store.CONSUL:
|
||||
consul.Register()
|
||||
case store.ETCD:
|
||||
etcd.Register()
|
||||
case store.ZK:
|
||||
zookeeper.Register()
|
||||
case store.BOLTDB:
|
||||
boltdb.Register()
|
||||
default:
|
||||
return errors.New("Invalid kv store: " + string(provider.StoreType))
|
||||
storeConfig := &store.Config{
|
||||
ConnectionTimeout: 30 * time.Second,
|
||||
Bucket: "traefik",
|
||||
}
|
||||
|
||||
if provider.TLS != nil {
|
||||
caPool := x509.NewCertPool()
|
||||
|
||||
if provider.TLS.CA != "" {
|
||||
ca, err := ioutil.ReadFile(provider.TLS.CA)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to read CA. %s", err)
|
||||
}
|
||||
|
||||
caPool.AppendCertsFromPEM(ca)
|
||||
}
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(provider.TLS.Cert, provider.TLS.Key)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to load keypair. %s", err)
|
||||
}
|
||||
|
||||
storeConfig.TLS = &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: caPool,
|
||||
InsecureSkipVerify: provider.TLS.InsecureSkipVerify,
|
||||
}
|
||||
}
|
||||
|
||||
kv, err := libkv.NewStore(
|
||||
provider.StoreType,
|
||||
[]string{provider.Endpoint},
|
||||
&store.Config{
|
||||
ConnectionTimeout: 30 * time.Second,
|
||||
Bucket: "traefik",
|
||||
},
|
||||
provider.storeType,
|
||||
strings.Split(provider.Endpoint, ","),
|
||||
storeConfig,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -104,98 +102,81 @@ func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage) error
|
||||
}
|
||||
provider.kvclient = kv
|
||||
if provider.Watch {
|
||||
stopCh := make(chan struct{})
|
||||
chanKeys, err := kv.WatchTree(provider.Prefix, stopCh)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
for {
|
||||
<-chanKeys
|
||||
configuration := provider.loadConfig()
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{string(provider.StoreType), configuration}
|
||||
}
|
||||
defer close(stopCh)
|
||||
}
|
||||
}()
|
||||
safe.Go(func() {
|
||||
provider.watchKv(configurationChan, provider.Prefix)
|
||||
})
|
||||
}
|
||||
configuration := provider.loadConfig()
|
||||
configurationChan <- types.ConfigMessage{string(provider.StoreType), configuration}
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: string(provider.storeType),
|
||||
Configuration: configuration,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Kv) loadConfig() *types.Configuration {
|
||||
configuration := new(types.Configuration)
|
||||
templateObjects := struct {
|
||||
Prefix string
|
||||
}{
|
||||
provider.Prefix,
|
||||
// Allow `/traefik/alias` to superesede `provider.Prefix`
|
||||
strings.TrimSuffix(provider.get(provider.Prefix, provider.Prefix+"/alias"), "/"),
|
||||
}
|
||||
var KvFuncMap = template.FuncMap{
|
||||
"List": func(keys ...string) []string {
|
||||
joinedKeys := strings.Join(keys, "")
|
||||
keysPairs, err := provider.kvclient.List(joinedKeys)
|
||||
if err != nil {
|
||||
log.Error("Error getting keys: ", joinedKeys, err)
|
||||
return nil
|
||||
}
|
||||
directoryKeys := make(map[string]string)
|
||||
for _, key := range keysPairs {
|
||||
directory := strings.Split(strings.TrimPrefix(key.Key, strings.TrimPrefix(joinedKeys, "/")), "/")[0]
|
||||
directoryKeys[directory] = joinedKeys + directory
|
||||
}
|
||||
return fun.Values(directoryKeys).([]string)
|
||||
},
|
||||
"Get": func(keys ...string) string {
|
||||
joinedKeys := strings.Join(keys, "")
|
||||
keyPair, err := provider.kvclient.Get(joinedKeys)
|
||||
if err != nil {
|
||||
log.Debug("Error getting key: ", joinedKeys, err)
|
||||
return ""
|
||||
} else if keyPair == nil {
|
||||
return ""
|
||||
}
|
||||
return string(keyPair.Value)
|
||||
},
|
||||
"Last": func(key string) string {
|
||||
splittedKey := strings.Split(key, "/")
|
||||
return splittedKey[len(splittedKey)-1]
|
||||
},
|
||||
"List": provider.list,
|
||||
"Get": provider.get,
|
||||
"SplitGet": provider.splitGet,
|
||||
"Last": provider.last,
|
||||
}
|
||||
|
||||
tmpl := template.New(provider.Filename).Funcs(KvFuncMap)
|
||||
if len(provider.Filename) > 0 {
|
||||
_, err := tmpl.ParseFiles(provider.Filename)
|
||||
if err != nil {
|
||||
log.Error("Error reading file", err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
buf, err := autogen.Asset("templates/kv.tmpl")
|
||||
if err != nil {
|
||||
log.Error("Error reading file", err)
|
||||
}
|
||||
_, err = tmpl.Parse(string(buf))
|
||||
if err != nil {
|
||||
log.Error("Error reading file", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
err := tmpl.Execute(&buffer, templateObjects)
|
||||
configuration, err := provider.getConfiguration("templates/kv.tmpl", KvFuncMap, templateObjects)
|
||||
if err != nil {
|
||||
log.Error("Error with kv template:", err)
|
||||
return nil
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if _, err := toml.Decode(buffer.String(), configuration); err != nil {
|
||||
log.Error("Error creating kv configuration:", err)
|
||||
log.Error(buffer.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
return configuration
|
||||
}
|
||||
|
||||
func (provider *Kv) list(keys ...string) []string {
|
||||
joinedKeys := strings.Join(keys, "")
|
||||
keysPairs, err := provider.kvclient.List(joinedKeys)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting keys %s %s ", joinedKeys, err)
|
||||
return nil
|
||||
}
|
||||
directoryKeys := make(map[string]string)
|
||||
for _, key := range keysPairs {
|
||||
directory := strings.Split(strings.TrimPrefix(key.Key, strings.TrimPrefix(joinedKeys, "/")), "/")[0]
|
||||
directoryKeys[directory] = joinedKeys + directory
|
||||
}
|
||||
return fun.Values(directoryKeys).([]string)
|
||||
}
|
||||
|
||||
func (provider *Kv) get(defaultValue string, keys ...string) string {
|
||||
joinedKeys := strings.Join(keys, "")
|
||||
keyPair, err := provider.kvclient.Get(joinedKeys)
|
||||
if err != nil {
|
||||
log.Warnf("Error getting key %s %s, setting default %s", joinedKeys, err, defaultValue)
|
||||
return defaultValue
|
||||
} else if keyPair == nil {
|
||||
log.Warnf("Error getting key %s, setting default %s", joinedKeys, defaultValue)
|
||||
return defaultValue
|
||||
}
|
||||
return string(keyPair.Value)
|
||||
}
|
||||
|
||||
func (provider *Kv) splitGet(keys ...string) []string {
|
||||
joinedKeys := strings.Join(keys, "")
|
||||
keyPair, err := provider.kvclient.Get(joinedKeys)
|
||||
if err != nil {
|
||||
log.Warnf("Error getting key %s %s, setting default empty", joinedKeys, err)
|
||||
return []string{}
|
||||
} else if keyPair == nil {
|
||||
log.Warnf("Error getting key %s, setting default %empty", joinedKeys)
|
||||
return []string{}
|
||||
}
|
||||
return strings.Split(string(keyPair.Value), ",")
|
||||
}
|
||||
|
||||
func (provider *Kv) last(key string) string {
|
||||
splittedKey := strings.Split(key, "/")
|
||||
return splittedKey[len(splittedKey)-1]
|
||||
}
|
||||
|
||||
367
provider/kv_test.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/containous/traefik/types"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/docker/libkv/store"
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func TestKvList(t *testing.T) {
|
||||
cases := []struct {
|
||||
provider *Kv
|
||||
keys []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
provider: &Kv{
|
||||
kvclient: &Mock{},
|
||||
},
|
||||
keys: []string{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
provider: &Kv{
|
||||
kvclient: &Mock{},
|
||||
},
|
||||
keys: []string{"traefik"},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
provider: &Kv{
|
||||
kvclient: &Mock{
|
||||
KVPairs: []*store.KVPair{
|
||||
{
|
||||
Key: "foo",
|
||||
Value: []byte("bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
keys: []string{"bar"},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
provider: &Kv{
|
||||
kvclient: &Mock{
|
||||
KVPairs: []*store.KVPair{
|
||||
{
|
||||
Key: "foo",
|
||||
Value: []byte("bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
keys: []string{"foo"},
|
||||
expected: []string{"foo"},
|
||||
},
|
||||
{
|
||||
provider: &Kv{
|
||||
kvclient: &Mock{
|
||||
KVPairs: []*store.KVPair{
|
||||
{
|
||||
Key: "foo/baz/1",
|
||||
Value: []byte("bar"),
|
||||
},
|
||||
{
|
||||
Key: "foo/baz/2",
|
||||
Value: []byte("bar"),
|
||||
},
|
||||
{
|
||||
Key: "foo/baz/biz/1",
|
||||
Value: []byte("bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
keys: []string{"foo", "/baz/"},
|
||||
expected: []string{"foo/baz/biz", "foo/baz/1", "foo/baz/2"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actual := c.provider.list(c.keys...)
|
||||
sort.Strings(actual)
|
||||
sort.Strings(c.expected)
|
||||
if !reflect.DeepEqual(actual, c.expected) {
|
||||
t.Fatalf("expected %v, got %v for %v and %v", c.expected, actual, c.keys, c.provider)
|
||||
}
|
||||
}
|
||||
|
||||
// Error case
|
||||
provider := &Kv{
|
||||
kvclient: &Mock{
|
||||
Error: true,
|
||||
},
|
||||
}
|
||||
actual := provider.list("anything")
|
||||
if actual != nil {
|
||||
t.Fatalf("Should have return nil, got %v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKvGet(t *testing.T) {
|
||||
cases := []struct {
|
||||
provider *Kv
|
||||
keys []string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
provider: &Kv{
|
||||
kvclient: &Mock{},
|
||||
},
|
||||
keys: []string{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
provider: &Kv{
|
||||
kvclient: &Mock{},
|
||||
},
|
||||
keys: []string{"traefik"},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
provider: &Kv{
|
||||
kvclient: &Mock{
|
||||
KVPairs: []*store.KVPair{
|
||||
{
|
||||
Key: "foo",
|
||||
Value: []byte("bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
keys: []string{"bar"},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
provider: &Kv{
|
||||
kvclient: &Mock{
|
||||
KVPairs: []*store.KVPair{
|
||||
{
|
||||
Key: "foo",
|
||||
Value: []byte("bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
keys: []string{"foo"},
|
||||
expected: "bar",
|
||||
},
|
||||
{
|
||||
provider: &Kv{
|
||||
kvclient: &Mock{
|
||||
KVPairs: []*store.KVPair{
|
||||
{
|
||||
Key: "foo/baz/1",
|
||||
Value: []byte("bar1"),
|
||||
},
|
||||
{
|
||||
Key: "foo/baz/2",
|
||||
Value: []byte("bar2"),
|
||||
},
|
||||
{
|
||||
Key: "foo/baz/biz/1",
|
||||
Value: []byte("bar3"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
keys: []string{"foo", "/baz/", "2"},
|
||||
expected: "bar2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actual := c.provider.get("", c.keys...)
|
||||
if actual != c.expected {
|
||||
t.Fatalf("expected %v, got %v for %v and %v", c.expected, actual, c.keys, c.provider)
|
||||
}
|
||||
}
|
||||
|
||||
// Error case
|
||||
provider := &Kv{
|
||||
kvclient: &Mock{
|
||||
Error: true,
|
||||
},
|
||||
}
|
||||
actual := provider.get("", "anything")
|
||||
if actual != "" {
|
||||
t.Fatalf("Should have return nil, got %v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestKvLast(t *testing.T) {
|
||||
cases := []struct {
|
||||
key string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
key: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
key: "foo",
|
||||
expected: "foo",
|
||||
},
|
||||
{
|
||||
key: "foo/bar",
|
||||
expected: "bar",
|
||||
},
|
||||
{
|
||||
key: "foo/bar/baz",
|
||||
expected: "baz",
|
||||
},
|
||||
// FIXME is this wanted ?
|
||||
{
|
||||
key: "foo/bar/",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
provider := &Kv{}
|
||||
for _, c := range cases {
|
||||
actual := provider.last(c.key)
|
||||
if actual != c.expected {
|
||||
t.Fatalf("expected %s, got %s", c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type KvMock struct {
|
||||
Kv
|
||||
}
|
||||
|
||||
func (provider *KvMock) loadConfig() *types.Configuration {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestKvWatchTree(t *testing.T) {
|
||||
returnedChans := make(chan chan []*store.KVPair)
|
||||
provider := &KvMock{
|
||||
Kv{
|
||||
kvclient: &Mock{
|
||||
WatchTreeMethod: func() <-chan []*store.KVPair {
|
||||
c := make(chan []*store.KVPair, 10)
|
||||
returnedChans <- c
|
||||
return c
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
configChan := make(chan types.ConfigMessage)
|
||||
safe.Go(func() {
|
||||
provider.watchKv(configChan, "prefix")
|
||||
})
|
||||
|
||||
select {
|
||||
case c1 := <-returnedChans:
|
||||
c1 <- []*store.KVPair{}
|
||||
<-configChan
|
||||
close(c1) // WatchTree chans can close due to error
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("Failed to create a new WatchTree chan")
|
||||
}
|
||||
|
||||
select {
|
||||
case c2 := <-returnedChans:
|
||||
c2 <- []*store.KVPair{}
|
||||
<-configChan
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("Failed to create a new WatchTree chan")
|
||||
}
|
||||
|
||||
select {
|
||||
case _ = <-configChan:
|
||||
t.Fatalf("configChan should be empty")
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Extremely limited mock store so we can test initialization
|
||||
type Mock struct {
|
||||
Error bool
|
||||
KVPairs []*store.KVPair
|
||||
WatchTreeMethod func() <-chan []*store.KVPair
|
||||
}
|
||||
|
||||
func (s *Mock) Put(key string, value []byte, opts *store.WriteOptions) error {
|
||||
return errors.New("Put not supported")
|
||||
}
|
||||
|
||||
func (s *Mock) Get(key string) (*store.KVPair, error) {
|
||||
if s.Error {
|
||||
return nil, errors.New("Error")
|
||||
}
|
||||
for _, kvPair := range s.KVPairs {
|
||||
if kvPair.Key == key {
|
||||
return kvPair, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *Mock) Delete(key string) error {
|
||||
return errors.New("Delete not supported")
|
||||
}
|
||||
|
||||
// Exists mock
|
||||
func (s *Mock) Exists(key string) (bool, error) {
|
||||
return false, errors.New("Exists not supported")
|
||||
}
|
||||
|
||||
// Watch mock
|
||||
func (s *Mock) Watch(key string, stopCh <-chan struct{}) (<-chan *store.KVPair, error) {
|
||||
return nil, errors.New("Watch not supported")
|
||||
}
|
||||
|
||||
// WatchTree mock
|
||||
func (s *Mock) WatchTree(prefix string, stopCh <-chan struct{}) (<-chan []*store.KVPair, error) {
|
||||
return s.WatchTreeMethod(), nil
|
||||
}
|
||||
|
||||
// NewLock mock
|
||||
func (s *Mock) NewLock(key string, options *store.LockOptions) (store.Locker, error) {
|
||||
return nil, errors.New("NewLock not supported")
|
||||
}
|
||||
|
||||
// List mock
|
||||
func (s *Mock) List(prefix string) ([]*store.KVPair, error) {
|
||||
if s.Error {
|
||||
return nil, errors.New("Error")
|
||||
}
|
||||
kv := []*store.KVPair{}
|
||||
for _, kvPair := range s.KVPairs {
|
||||
if strings.HasPrefix(kvPair.Key, prefix) {
|
||||
kv = append(kv, kvPair)
|
||||
}
|
||||
}
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
// DeleteTree mock
|
||||
func (s *Mock) DeleteTree(prefix string) error {
|
||||
return errors.New("DeleteTree not supported")
|
||||
}
|
||||
|
||||
// AtomicPut mock
|
||||
func (s *Mock) AtomicPut(key string, value []byte, previous *store.KVPair, opts *store.WriteOptions) (bool, *store.KVPair, error) {
|
||||
return false, nil, errors.New("AtomicPut not supported")
|
||||
}
|
||||
|
||||
// AtomicDelete mock
|
||||
func (s *Mock) AtomicDelete(key string, previous *store.KVPair) (bool, error) {
|
||||
return false, errors.New("AtomicDelete not supported")
|
||||
}
|
||||
|
||||
// Close mock
|
||||
func (s *Mock) Close() {
|
||||
return
|
||||
}
|
||||
@@ -1,36 +1,58 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"crypto/tls"
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/emilevauge/traefik/autogen"
|
||||
"github.com/emilevauge/traefik/types"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/gambol99/go-marathon"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Marathon holds configuration of the Marathon provider.
|
||||
type Marathon struct {
|
||||
Watch bool
|
||||
BaseProvider `mapstructure:",squash"`
|
||||
Endpoint string
|
||||
Domain string
|
||||
Filename string
|
||||
NetworkInterface string
|
||||
ExposedByDefault bool
|
||||
Basic *MarathonBasic
|
||||
TLS *tls.Config
|
||||
marathonClient marathon.Marathon
|
||||
}
|
||||
|
||||
// MarathonBasic holds basic authentication specific configurations
|
||||
type MarathonBasic struct {
|
||||
HTTPBasicAuthUser string
|
||||
HTTPBasicPassword string
|
||||
}
|
||||
|
||||
type lightMarathonClient interface {
|
||||
Applications(url.Values) (*marathon.Applications, error)
|
||||
AllTasks(v url.Values) (*marathon.Tasks, error)
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
config := marathon.NewDefaultConfig()
|
||||
config.URL = provider.Endpoint
|
||||
config.EventsInterface = provider.NetworkInterface
|
||||
config.EventsTransport = marathon.EventsTransportSSE
|
||||
if provider.Basic != nil {
|
||||
config.HTTPBasicAuthUser = provider.Basic.HTTPBasicAuthUser
|
||||
config.HTTPBasicPassword = provider.Basic.HTTPBasicPassword
|
||||
}
|
||||
config.HTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: provider.TLS,
|
||||
},
|
||||
}
|
||||
client, err := marathon.NewClient(config)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create a client for marathon, error: %s", err)
|
||||
@@ -40,75 +62,45 @@ func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage)
|
||||
update := make(marathon.EventsChannel, 5)
|
||||
if provider.Watch {
|
||||
if err := client.AddEventsListener(update, marathon.EVENTS_APPLICATIONS); err != nil {
|
||||
log.Errorf("Failed to register for subscriptions, %s", err)
|
||||
log.Errorf("Failed to register for events, %s", err)
|
||||
} else {
|
||||
go func() {
|
||||
safe.Go(func() {
|
||||
for {
|
||||
event := <-update
|
||||
log.Debug("Marathon event receveived", event)
|
||||
configuration := provider.loadMarathonConfig()
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{"marathon", configuration}
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "marathon",
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
configuration := provider.loadMarathonConfig()
|
||||
configurationChan <- types.ConfigMessage{"marathon", configuration}
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "marathon",
|
||||
Configuration: configuration,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Marathon) loadMarathonConfig() *types.Configuration {
|
||||
var MarathonFuncMap = template.FuncMap{
|
||||
"getPort": func(task marathon.Task) string {
|
||||
for _, port := range task.Ports {
|
||||
return strconv.Itoa(port)
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"getWeight": func(task marathon.Task, applications []marathon.Application) string {
|
||||
application, errApp := getApplication(task, applications)
|
||||
if errApp != nil {
|
||||
log.Errorf("Unable to get marathon application from task %s", task.AppID)
|
||||
return "0"
|
||||
}
|
||||
if label, err := provider.getLabel(application, "traefik.weight"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "0"
|
||||
},
|
||||
"getDomain": func(application marathon.Application) string {
|
||||
if label, err := provider.getLabel(application, "traefik.domain"); err == nil {
|
||||
return label
|
||||
}
|
||||
return provider.Domain
|
||||
},
|
||||
"replace": func(s1 string, s2 string, s3 string) string {
|
||||
return strings.Replace(s3, s1, s2, -1)
|
||||
},
|
||||
"getProtocol": func(task marathon.Task, applications []marathon.Application) string {
|
||||
application, errApp := getApplication(task, applications)
|
||||
if errApp != nil {
|
||||
log.Errorf("Unable to get marathon application from task %s", task.AppID)
|
||||
return "http"
|
||||
}
|
||||
if label, err := provider.getLabel(application, "traefik.protocol"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "http"
|
||||
},
|
||||
"getPassHostHeader": func(application marathon.Application) string {
|
||||
if passHostHeader, err := provider.getLabel(application, "traefik.frontend.passHostHeader"); err == nil {
|
||||
return passHostHeader
|
||||
}
|
||||
return "false"
|
||||
},
|
||||
"getFrontendValue": provider.GetFrontendValue,
|
||||
"getFrontendRule": provider.GetFrontendRule,
|
||||
"getBackend": provider.getBackend,
|
||||
"getPort": provider.getPort,
|
||||
"getWeight": provider.getWeight,
|
||||
"getDomain": provider.getDomain,
|
||||
"getProtocol": provider.getProtocol,
|
||||
"getPassHostHeader": provider.getPassHostHeader,
|
||||
"getEntryPoints": provider.getEntryPoints,
|
||||
"getFrontendRule": provider.getFrontendRule,
|
||||
"getFrontendBackend": provider.getFrontendBackend,
|
||||
"replace": replace,
|
||||
}
|
||||
configuration := new(types.Configuration)
|
||||
|
||||
applications, err := provider.marathonClient.Applications(nil)
|
||||
if err != nil {
|
||||
@@ -116,7 +108,7 @@ func (provider *Marathon) loadMarathonConfig() *types.Configuration {
|
||||
return nil
|
||||
}
|
||||
|
||||
tasks, err := provider.marathonClient.AllTasks()
|
||||
tasks, err := provider.marathonClient.AllTasks(&marathon.AllTasksOpts{Status: "running"})
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create a client for marathon, error: %s", err)
|
||||
return nil
|
||||
@@ -124,54 +116,12 @@ func (provider *Marathon) loadMarathonConfig() *types.Configuration {
|
||||
|
||||
//filter tasks
|
||||
filteredTasks := fun.Filter(func(task marathon.Task) bool {
|
||||
if len(task.Ports) == 0 {
|
||||
log.Debug("Filtering marathon task without port %s", task.AppID)
|
||||
return false
|
||||
}
|
||||
application, errApp := getApplication(task, applications.Apps)
|
||||
if errApp != nil {
|
||||
log.Errorf("Unable to get marathon application from task %s", task.AppID)
|
||||
return false
|
||||
}
|
||||
_, err := strconv.Atoi(application.Labels["traefik.port"])
|
||||
if len(application.Ports) > 1 && err != nil {
|
||||
log.Debugf("Filtering marathon task %s with more than 1 port and no traefik.port label", task.AppID)
|
||||
return false
|
||||
}
|
||||
if application.Labels["traefik.enable"] == "false" {
|
||||
log.Debugf("Filtering disabled marathon task %s", task.AppID)
|
||||
return false
|
||||
}
|
||||
//filter healthchecks
|
||||
if application.HasHealthChecks() {
|
||||
if task.HasHealthCheckResults() {
|
||||
for _, healthcheck := range task.HealthCheckResult {
|
||||
// found one bad healthcheck, return false
|
||||
if !healthcheck.Alive {
|
||||
log.Debugf("Filtering marathon task %s with bad healthcheck", task.AppID)
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Filtering marathon task %s with bad healthcheck", task.AppID)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return taskFilter(task, applications, provider.ExposedByDefault)
|
||||
}, tasks.Tasks).([]marathon.Task)
|
||||
|
||||
//filter apps
|
||||
filteredApps := fun.Filter(func(app marathon.Application) bool {
|
||||
//get ports from app tasks
|
||||
if !fun.Exists(func(task marathon.Task) bool {
|
||||
if task.AppID == app.ID {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}, filteredTasks) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return applicationFilter(app, filteredTasks)
|
||||
}, applications.Apps).([]marathon.Application)
|
||||
|
||||
templateObjects := struct {
|
||||
@@ -184,41 +134,92 @@ func (provider *Marathon) loadMarathonConfig() *types.Configuration {
|
||||
provider.Domain,
|
||||
}
|
||||
|
||||
tmpl := template.New(provider.Filename).Funcs(MarathonFuncMap)
|
||||
if len(provider.Filename) > 0 {
|
||||
_, err := tmpl.ParseFiles(provider.Filename)
|
||||
if err != nil {
|
||||
log.Error("Error reading file", err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
buf, err := autogen.Asset("templates/marathon.tmpl")
|
||||
if err != nil {
|
||||
log.Error("Error reading file", err)
|
||||
}
|
||||
_, err = tmpl.Parse(string(buf))
|
||||
if err != nil {
|
||||
log.Error("Error reading file", err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
err = tmpl.Execute(&buffer, templateObjects)
|
||||
configuration, err := provider.getConfiguration("templates/marathon.tmpl", MarathonFuncMap, templateObjects)
|
||||
if err != nil {
|
||||
log.Error("Error with marathon template:", err)
|
||||
return nil
|
||||
log.Error(err)
|
||||
}
|
||||
|
||||
if _, err := toml.Decode(buffer.String(), configuration); err != nil {
|
||||
log.Error("Error creating marathon configuration:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return configuration
|
||||
}
|
||||
|
||||
func taskFilter(task marathon.Task, applications *marathon.Applications, exposedByDefaultFlag bool) bool {
|
||||
if len(task.Ports) == 0 {
|
||||
log.Debug("Filtering marathon task without port %s", task.AppID)
|
||||
return false
|
||||
}
|
||||
application, err := getApplication(task, applications.Apps)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to get marathon application from task %s", task.AppID)
|
||||
return false
|
||||
}
|
||||
|
||||
if !isApplicationEnabled(application, exposedByDefaultFlag) {
|
||||
log.Debugf("Filtering disabled marathon task %s", task.AppID)
|
||||
return false
|
||||
}
|
||||
|
||||
//filter indeterminable task port
|
||||
portIndexLabel := application.Labels["traefik.portIndex"]
|
||||
portValueLabel := application.Labels["traefik.port"]
|
||||
if portIndexLabel != "" && portValueLabel != "" {
|
||||
log.Debugf("Filtering marathon task %s specifying both traefik.portIndex and traefik.port labels", task.AppID)
|
||||
return false
|
||||
}
|
||||
if portIndexLabel == "" && portValueLabel == "" && len(application.Ports) > 1 {
|
||||
log.Debugf("Filtering marathon task %s with more than 1 port and no traefik.portIndex or traefik.port label", task.AppID)
|
||||
return false
|
||||
}
|
||||
if portIndexLabel != "" {
|
||||
index, err := strconv.Atoi(application.Labels["traefik.portIndex"])
|
||||
if err != nil || index < 0 || index > len(application.Ports)-1 {
|
||||
log.Debugf("Filtering marathon task %s with unexpected value for traefik.portIndex label", task.AppID)
|
||||
return false
|
||||
}
|
||||
}
|
||||
if portValueLabel != "" {
|
||||
port, err := strconv.Atoi(application.Labels["traefik.port"])
|
||||
if err != nil {
|
||||
log.Debugf("Filtering marathon task %s with unexpected value for traefik.port label", task.AppID)
|
||||
return false
|
||||
}
|
||||
|
||||
var foundPort bool
|
||||
for _, exposedPort := range task.Ports {
|
||||
if port == exposedPort {
|
||||
foundPort = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundPort {
|
||||
log.Debugf("Filtering marathon task %s without a matching port for traefik.port label", task.AppID)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
//filter healthchecks
|
||||
if application.HasHealthChecks() {
|
||||
if task.HasHealthCheckResults() {
|
||||
for _, healthcheck := range task.HealthCheckResults {
|
||||
// found one bad healthcheck, return false
|
||||
if !healthcheck.Alive {
|
||||
log.Debugf("Filtering marathon task %s with bad healthcheck", task.AppID)
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Filtering marathon task %s with bad healthcheck", task.AppID)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationFilter(app marathon.Application, filteredTasks []marathon.Task) bool {
|
||||
return fun.Exists(func(task marathon.Task) bool {
|
||||
return task.AppID == app.ID
|
||||
}, filteredTasks)
|
||||
}
|
||||
|
||||
func getApplication(task marathon.Task, apps []marathon.Application) (marathon.Application, error) {
|
||||
for _, application := range apps {
|
||||
if application.ID == task.AppID {
|
||||
@@ -228,6 +229,10 @@ func getApplication(task marathon.Task, apps []marathon.Application) (marathon.A
|
||||
return marathon.Application{}, errors.New("Application not found: " + task.AppID)
|
||||
}
|
||||
|
||||
func isApplicationEnabled(application marathon.Application, exposedByDefault bool) bool {
|
||||
return exposedByDefault && application.Labels["traefik.enable"] != "false" || application.Labels["traefik.enable"] == "true"
|
||||
}
|
||||
|
||||
func (provider *Marathon) getLabel(application marathon.Application, label string) (string, error) {
|
||||
for key, value := range application.Labels {
|
||||
if key == label {
|
||||
@@ -237,24 +242,102 @@ func (provider *Marathon) getLabel(application marathon.Application, label strin
|
||||
return "", errors.New("Label not found:" + label)
|
||||
}
|
||||
|
||||
func (provider *Marathon) getEscapedName(name string) string {
|
||||
return strings.Replace(name, "/", "", -1)
|
||||
func (provider *Marathon) getPort(task marathon.Task, applications []marathon.Application) string {
|
||||
application, err := getApplication(task, applications)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to get marathon application from task %s", task.AppID)
|
||||
return ""
|
||||
}
|
||||
|
||||
if portIndexLabel, err := provider.getLabel(application, "traefik.portIndex"); err == nil {
|
||||
if index, err := strconv.Atoi(portIndexLabel); err == nil {
|
||||
return strconv.Itoa(task.Ports[index])
|
||||
}
|
||||
}
|
||||
if portValueLabel, err := provider.getLabel(application, "traefik.port"); err == nil {
|
||||
return portValueLabel
|
||||
}
|
||||
|
||||
for _, port := range task.Ports {
|
||||
return strconv.Itoa(port)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetFrontendValue returns the frontend value for the specified application, using
|
||||
// it's label. It returns a default one if the label is not present.
|
||||
func (provider *Marathon) GetFrontendValue(application marathon.Application) string {
|
||||
if label, err := provider.getLabel(application, "traefik.frontend.value"); err == nil {
|
||||
func (provider *Marathon) getWeight(task marathon.Task, applications []marathon.Application) string {
|
||||
application, errApp := getApplication(task, applications)
|
||||
if errApp != nil {
|
||||
log.Errorf("Unable to get marathon application from task %s", task.AppID)
|
||||
return "0"
|
||||
}
|
||||
if label, err := provider.getLabel(application, "traefik.weight"); err == nil {
|
||||
return label
|
||||
}
|
||||
return provider.getEscapedName(application.ID) + "." + provider.Domain
|
||||
return "0"
|
||||
}
|
||||
|
||||
// GetFrontendRule returns the frontend rule for the specified application, using
|
||||
func (provider *Marathon) getDomain(application marathon.Application) string {
|
||||
if label, err := provider.getLabel(application, "traefik.domain"); err == nil {
|
||||
return label
|
||||
}
|
||||
return provider.Domain
|
||||
}
|
||||
|
||||
func (provider *Marathon) getProtocol(task marathon.Task, applications []marathon.Application) string {
|
||||
application, errApp := getApplication(task, applications)
|
||||
if errApp != nil {
|
||||
log.Errorf("Unable to get marathon application from task %s", task.AppID)
|
||||
return "http"
|
||||
}
|
||||
if label, err := provider.getLabel(application, "traefik.protocol"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (provider *Marathon) getPassHostHeader(application marathon.Application) string {
|
||||
if passHostHeader, err := provider.getLabel(application, "traefik.frontend.passHostHeader"); err == nil {
|
||||
return passHostHeader
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func (provider *Marathon) getEntryPoints(application marathon.Application) []string {
|
||||
if entryPoints, err := provider.getLabel(application, "traefik.frontend.entryPoints"); err == nil {
|
||||
return strings.Split(entryPoints, ",")
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// getFrontendRule returns the frontend rule for the specified application, using
|
||||
// it's label. It returns a default one (Host) if the label is not present.
|
||||
func (provider *Marathon) GetFrontendRule(application marathon.Application) string {
|
||||
func (provider *Marathon) getFrontendRule(application marathon.Application) string {
|
||||
// ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
|
||||
// TODO: backwards compatibility with DEPRECATED rule.Value
|
||||
if value, err := provider.getLabel(application, "traefik.frontend.value"); err == nil {
|
||||
log.Warnf("Label traefik.frontend.value=%s is DEPRECATED, please refer to the rule label: https://github.com/containous/traefik/blob/master/docs/index.md#marathon", value)
|
||||
rule, _ := provider.getLabel(application, "traefik.frontend.rule")
|
||||
return rule + ":" + value
|
||||
}
|
||||
// ⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠⚠
|
||||
if label, err := provider.getLabel(application, "traefik.frontend.rule"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "Host"
|
||||
return "Host:" + getEscapedName(application.ID) + "." + provider.Domain
|
||||
}
|
||||
|
||||
func (provider *Marathon) getBackend(task marathon.Task, applications []marathon.Application) string {
|
||||
application, errApp := getApplication(task, applications)
|
||||
if errApp != nil {
|
||||
log.Errorf("Unable to get marathon application from task %s", task.AppID)
|
||||
return ""
|
||||
}
|
||||
return provider.getFrontendBackend(application)
|
||||
}
|
||||
|
||||
func (provider *Marathon) getFrontendBackend(application marathon.Application) string {
|
||||
if label, err := provider.getLabel(application, "traefik.backend"); err == nil {
|
||||
return label
|
||||
}
|
||||
return replace("/", "-", application.ID)
|
||||
}
|
||||
|
||||
894
provider/marathon_test.go
Normal file
@@ -0,0 +1,894 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"errors"
|
||||
"github.com/containous/traefik/mocks"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/gambol99/go-marathon"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type fakeClient struct {
|
||||
mocks.Marathon
|
||||
}
|
||||
|
||||
func newFakeClient(applicationsError bool, applications *marathon.Applications, tasksError bool, tasks *marathon.Tasks) *fakeClient {
|
||||
// create an instance of our test object
|
||||
fakeClient := new(fakeClient)
|
||||
if applicationsError {
|
||||
fakeClient.On("Applications", mock.AnythingOfType("url.Values")).Return(nil, errors.New("error"))
|
||||
}
|
||||
fakeClient.On("Applications", mock.AnythingOfType("url.Values")).Return(applications, nil)
|
||||
if tasksError {
|
||||
fakeClient.On("AllTasks", mock.AnythingOfType("*marathon.AllTasksOpts")).Return(nil, errors.New("error"))
|
||||
}
|
||||
fakeClient.On("AllTasks", mock.AnythingOfType("*marathon.AllTasksOpts")).Return(tasks, nil)
|
||||
return fakeClient
|
||||
}
|
||||
|
||||
func TestMarathonLoadConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
applicationsError bool
|
||||
applications *marathon.Applications
|
||||
tasksError bool
|
||||
tasks *marathon.Tasks
|
||||
expectedNil bool
|
||||
expectedFrontends map[string]*types.Frontend
|
||||
expectedBackends map[string]*types.Backend
|
||||
}{
|
||||
{
|
||||
applications: &marathon.Applications{},
|
||||
tasks: &marathon.Tasks{},
|
||||
expectedFrontends: map[string]*types.Frontend{},
|
||||
expectedBackends: map[string]*types.Backend{},
|
||||
},
|
||||
{
|
||||
applicationsError: true,
|
||||
applications: &marathon.Applications{},
|
||||
tasks: &marathon.Tasks{},
|
||||
expectedNil: true,
|
||||
expectedFrontends: map[string]*types.Frontend{},
|
||||
expectedBackends: map[string]*types.Backend{},
|
||||
},
|
||||
{
|
||||
applications: &marathon.Applications{},
|
||||
tasksError: true,
|
||||
tasks: &marathon.Tasks{},
|
||||
expectedNil: true,
|
||||
expectedFrontends: map[string]*types.Frontend{},
|
||||
expectedBackends: map[string]*types.Backend{},
|
||||
},
|
||||
{
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "/test",
|
||||
Ports: []int{80},
|
||||
},
|
||||
},
|
||||
},
|
||||
tasks: &marathon.Tasks{
|
||||
Tasks: []marathon.Task{
|
||||
{
|
||||
ID: "test",
|
||||
AppID: "/test",
|
||||
Host: "127.0.0.1",
|
||||
Ports: []int{80},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedFrontends: map[string]*types.Frontend{
|
||||
`frontend-test`: {
|
||||
Backend: "backend-test",
|
||||
EntryPoints: []string{},
|
||||
Routes: map[string]types.Route{
|
||||
`route-host-test`: {
|
||||
Rule: "Host:test.docker.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedBackends: map[string]*types.Backend{
|
||||
"backend-test": {
|
||||
Servers: map[string]types.Server{
|
||||
"server-test": {
|
||||
URL: "http://127.0.0.1:80",
|
||||
Weight: 0,
|
||||
},
|
||||
},
|
||||
CircuitBreaker: nil,
|
||||
LoadBalancer: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
fakeClient := newFakeClient(c.applicationsError, c.applications, c.tasksError, c.tasks)
|
||||
provider := &Marathon{
|
||||
Domain: "docker.localhost",
|
||||
ExposedByDefault: true,
|
||||
marathonClient: fakeClient,
|
||||
}
|
||||
actualConfig := provider.loadMarathonConfig()
|
||||
if c.expectedNil {
|
||||
if actualConfig != nil {
|
||||
t.Fatalf("Should have been nil, got %v", actualConfig)
|
||||
}
|
||||
} else {
|
||||
// Compare backends
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonTaskFilter(t *testing.T) {
|
||||
cases := []struct {
|
||||
task marathon.Task
|
||||
applications *marathon.Applications
|
||||
expected bool
|
||||
exposedByDefault bool
|
||||
}{
|
||||
{
|
||||
task: marathon.Task{},
|
||||
applications: &marathon.Applications{},
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "test",
|
||||
Ports: []int{80},
|
||||
},
|
||||
applications: &marathon.Applications{},
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "test",
|
||||
Ports: []int{80},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "foo",
|
||||
Ports: []int{80},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "foo",
|
||||
Ports: []int{80, 443},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "foo",
|
||||
Ports: []int{80},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "foo",
|
||||
Ports: []int{80},
|
||||
Labels: map[string]string{
|
||||
"traefik.enable": "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "specify-port-number",
|
||||
Ports: []int{80, 443},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "specify-port-number",
|
||||
Ports: []int{80, 443},
|
||||
Labels: map[string]string{
|
||||
"traefik.port": "80",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "specify-unknown-port-number",
|
||||
Ports: []int{80, 443},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "specify-unknown-port-number",
|
||||
Ports: []int{80, 443},
|
||||
Labels: map[string]string{
|
||||
"traefik.port": "8080",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "specify-port-index",
|
||||
Ports: []int{80, 443},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "specify-port-index",
|
||||
Ports: []int{80, 443},
|
||||
Labels: map[string]string{
|
||||
"traefik.portIndex": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "specify-out-of-range-port-index",
|
||||
Ports: []int{80, 443},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "specify-out-of-range-port-index",
|
||||
Ports: []int{80, 443},
|
||||
Labels: map[string]string{
|
||||
"traefik.portIndex": "2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "specify-both-port-index-and-number",
|
||||
Ports: []int{80, 443},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "specify-both-port-index-and-number",
|
||||
Ports: []int{80, 443},
|
||||
Labels: map[string]string{
|
||||
"traefik.port": "443",
|
||||
"traefik.portIndex": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "foo",
|
||||
Ports: []int{80},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "foo",
|
||||
Ports: []int{80},
|
||||
HealthChecks: []*marathon.HealthCheck{
|
||||
marathon.NewDefaultHealthCheck(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "foo",
|
||||
Ports: []int{80},
|
||||
HealthCheckResults: []*marathon.HealthCheckResult{
|
||||
{
|
||||
Alive: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "foo",
|
||||
Ports: []int{80},
|
||||
HealthChecks: []*marathon.HealthCheck{
|
||||
marathon.NewDefaultHealthCheck(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "foo",
|
||||
Ports: []int{80},
|
||||
HealthCheckResults: []*marathon.HealthCheckResult{
|
||||
{
|
||||
Alive: true,
|
||||
},
|
||||
{
|
||||
Alive: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "foo",
|
||||
Ports: []int{80},
|
||||
HealthChecks: []*marathon.HealthCheck{
|
||||
marathon.NewDefaultHealthCheck(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "foo",
|
||||
Ports: []int{80},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "foo",
|
||||
Ports: []int{80},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "foo",
|
||||
Ports: []int{80},
|
||||
HealthCheckResults: []*marathon.HealthCheckResult{
|
||||
{
|
||||
Alive: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "foo",
|
||||
Ports: []int{80},
|
||||
HealthChecks: []*marathon.HealthCheck{
|
||||
marathon.NewDefaultHealthCheck(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "disable-default-expose",
|
||||
Ports: []int{80},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "disable-default-expose",
|
||||
Ports: []int{80},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
exposedByDefault: false,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "disable-default-expose-disable-in-label",
|
||||
Ports: []int{80},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "disable-default-expose-disable-in-label",
|
||||
Ports: []int{80},
|
||||
Labels: map[string]string{
|
||||
"traefik.enable": "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
exposedByDefault: false,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "disable-default-expose-enable-in-label",
|
||||
Ports: []int{80},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "disable-default-expose-enable-in-label",
|
||||
Ports: []int{80},
|
||||
Labels: map[string]string{
|
||||
"traefik.enable": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
exposedByDefault: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actual := taskFilter(c.task, c.applications, c.exposedByDefault)
|
||||
if actual != c.expected {
|
||||
t.Fatalf("expected %v, got %v", c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonApplicationFilter(t *testing.T) {
|
||||
cases := []struct {
|
||||
application marathon.Application
|
||||
filteredTasks []marathon.Task
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
application: marathon.Application{},
|
||||
filteredTasks: []marathon.Task{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
application: marathon.Application{
|
||||
ID: "test",
|
||||
},
|
||||
filteredTasks: []marathon.Task{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
application: marathon.Application{
|
||||
ID: "foo",
|
||||
},
|
||||
filteredTasks: []marathon.Task{
|
||||
{
|
||||
AppID: "bar",
|
||||
},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
application: marathon.Application{
|
||||
ID: "foo",
|
||||
},
|
||||
filteredTasks: []marathon.Task{
|
||||
{
|
||||
AppID: "foo",
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actual := applicationFilter(c.application, c.filteredTasks)
|
||||
if actual != c.expected {
|
||||
t.Fatalf("expected %v, got %v", c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonGetPort(t *testing.T) {
|
||||
provider := &Marathon{}
|
||||
|
||||
cases := []struct {
|
||||
applications []marathon.Application
|
||||
task marathon.Task
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
applications: []marathon.Application{},
|
||||
task: marathon.Task{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "test1",
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "test2",
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "test1",
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "test1",
|
||||
Ports: []int{80},
|
||||
},
|
||||
expected: "80",
|
||||
},
|
||||
{
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "test1",
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "test1",
|
||||
Ports: []int{80, 443},
|
||||
},
|
||||
expected: "80",
|
||||
},
|
||||
{
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "specify-port-number",
|
||||
Labels: map[string]string{
|
||||
"traefik.port": "443",
|
||||
},
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "specify-port-number",
|
||||
Ports: []int{80, 443},
|
||||
},
|
||||
expected: "443",
|
||||
},
|
||||
{
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "specify-port-index",
|
||||
Labels: map[string]string{
|
||||
"traefik.portIndex": "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "specify-port-index",
|
||||
Ports: []int{80, 443},
|
||||
},
|
||||
expected: "443",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actual := provider.getPort(c.task, c.applications)
|
||||
if actual != c.expected {
|
||||
t.Fatalf("expected %q, got %q", c.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonGetWeigh(t *testing.T) {
|
||||
provider := &Marathon{}
|
||||
|
||||
applications := []struct {
|
||||
applications []marathon.Application
|
||||
task marathon.Task
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
applications: []marathon.Application{},
|
||||
task: marathon.Task{},
|
||||
expected: "0",
|
||||
},
|
||||
{
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "test1",
|
||||
Labels: map[string]string{
|
||||
"traefik.weight": "10",
|
||||
},
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "test2",
|
||||
},
|
||||
expected: "0",
|
||||
},
|
||||
{
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "test",
|
||||
Labels: map[string]string{
|
||||
"traefik.test": "10",
|
||||
},
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "test",
|
||||
},
|
||||
expected: "0",
|
||||
},
|
||||
{
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "test",
|
||||
Labels: map[string]string{
|
||||
"traefik.weight": "10",
|
||||
},
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "test",
|
||||
},
|
||||
expected: "10",
|
||||
},
|
||||
}
|
||||
|
||||
for _, a := range applications {
|
||||
actual := provider.getWeight(a.task, a.applications)
|
||||
if actual != a.expected {
|
||||
t.Fatalf("expected %q, got %q", a.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonGetDomain(t *testing.T) {
|
||||
provider := &Marathon{
|
||||
Domain: "docker.localhost",
|
||||
}
|
||||
|
||||
applications := []struct {
|
||||
application marathon.Application
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
application: marathon.Application{},
|
||||
expected: "docker.localhost",
|
||||
},
|
||||
{
|
||||
application: marathon.Application{
|
||||
Labels: map[string]string{
|
||||
"traefik.domain": "foo.bar",
|
||||
},
|
||||
},
|
||||
expected: "foo.bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, a := range applications {
|
||||
actual := provider.getDomain(a.application)
|
||||
if actual != a.expected {
|
||||
t.Fatalf("expected %q, got %q", a.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonGetProtocol(t *testing.T) {
|
||||
provider := &Marathon{}
|
||||
|
||||
applications := []struct {
|
||||
applications []marathon.Application
|
||||
task marathon.Task
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
applications: []marathon.Application{},
|
||||
task: marathon.Task{},
|
||||
expected: "http",
|
||||
},
|
||||
{
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "test1",
|
||||
Labels: map[string]string{
|
||||
"traefik.protocol": "https",
|
||||
},
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "test2",
|
||||
},
|
||||
expected: "http",
|
||||
},
|
||||
{
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "test",
|
||||
Labels: map[string]string{
|
||||
"traefik.foo": "bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "test",
|
||||
},
|
||||
expected: "http",
|
||||
},
|
||||
{
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "test",
|
||||
Labels: map[string]string{
|
||||
"traefik.protocol": "https",
|
||||
},
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "test",
|
||||
},
|
||||
expected: "https",
|
||||
},
|
||||
}
|
||||
|
||||
for _, a := range applications {
|
||||
actual := provider.getProtocol(a.task, a.applications)
|
||||
if actual != a.expected {
|
||||
t.Fatalf("expected %q, got %q", a.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonGetPassHostHeader(t *testing.T) {
|
||||
provider := &Marathon{}
|
||||
|
||||
applications := []struct {
|
||||
application marathon.Application
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
application: marathon.Application{},
|
||||
expected: "false",
|
||||
},
|
||||
{
|
||||
application: marathon.Application{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.passHostHeader": "true",
|
||||
},
|
||||
},
|
||||
expected: "true",
|
||||
},
|
||||
}
|
||||
|
||||
for _, a := range applications {
|
||||
actual := provider.getPassHostHeader(a.application)
|
||||
if actual != a.expected {
|
||||
t.Fatalf("expected %q, got %q", a.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonGetEntryPoints(t *testing.T) {
|
||||
provider := &Marathon{}
|
||||
|
||||
applications := []struct {
|
||||
application marathon.Application
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
application: marathon.Application{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
application: marathon.Application{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.entryPoints": "http,https",
|
||||
},
|
||||
},
|
||||
expected: []string{"http", "https"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, a := range applications {
|
||||
actual := provider.getEntryPoints(a.application)
|
||||
|
||||
if !reflect.DeepEqual(actual, a.expected) {
|
||||
t.Fatalf("expected %#v, got %#v", a.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonGetFrontendRule(t *testing.T) {
|
||||
provider := &Marathon{
|
||||
Domain: "docker.localhost",
|
||||
}
|
||||
|
||||
applications := []struct {
|
||||
application marathon.Application
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
application: marathon.Application{},
|
||||
expected: "Host:.docker.localhost",
|
||||
},
|
||||
{
|
||||
application: marathon.Application{
|
||||
ID: "test",
|
||||
},
|
||||
expected: "Host:test.docker.localhost",
|
||||
},
|
||||
{
|
||||
application: marathon.Application{
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
expected: "Host:foo.bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, a := range applications {
|
||||
actual := provider.getFrontendRule(a.application)
|
||||
if actual != a.expected {
|
||||
t.Fatalf("expected %q, got %q", a.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarathonGetBackend(t *testing.T) {
|
||||
provider := &Marathon{}
|
||||
|
||||
applications := []struct {
|
||||
application marathon.Application
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
application: marathon.Application{
|
||||
ID: "foo",
|
||||
Labels: map[string]string{
|
||||
"traefik.backend": "bar",
|
||||
},
|
||||
},
|
||||
expected: "bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, a := range applications {
|
||||
actual := provider.getFrontendBackend(a.application)
|
||||
if actual != a.expected {
|
||||
t.Fatalf("expected %q, got %q", a.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,16 @@
|
||||
package provider
|
||||
|
||||
import "github.com/emilevauge/traefik/types"
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/containous/traefik/autogen"
|
||||
"github.com/containous/traefik/types"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// Provider defines methods of a provider.
|
||||
type Provider interface {
|
||||
@@ -8,3 +18,61 @@ type Provider interface {
|
||||
// using the given configuration channel.
|
||||
Provide(configurationChan chan<- types.ConfigMessage) error
|
||||
}
|
||||
|
||||
// BaseProvider should be inherited by providers
|
||||
type BaseProvider struct {
|
||||
Watch bool
|
||||
Filename string
|
||||
}
|
||||
|
||||
func (p *BaseProvider) getConfiguration(defaultTemplateFile string, funcMap template.FuncMap, templateObjects interface{}) (*types.Configuration, error) {
|
||||
var (
|
||||
buf []byte
|
||||
err error
|
||||
)
|
||||
configuration := new(types.Configuration)
|
||||
tmpl := template.New(p.Filename).Funcs(funcMap)
|
||||
if len(p.Filename) > 0 {
|
||||
buf, err = ioutil.ReadFile(p.Filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
buf, err = autogen.Asset(defaultTemplateFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
_, err = tmpl.Parse(string(buf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
err = tmpl.Execute(&buffer, templateObjects)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := toml.Decode(buffer.String(), configuration); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return configuration, nil
|
||||
}
|
||||
|
||||
func replace(s1 string, s2 string, s3 string) string {
|
||||
return strings.Replace(s3, s1, s2, -1)
|
||||
}
|
||||
|
||||
// Escape beginning slash "/", convert all others to dash "-"
|
||||
func getEscapedName(name string) string {
|
||||
return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1)
|
||||
}
|
||||
|
||||
func normalize(name string) string {
|
||||
fargs := func(c rune) bool {
|
||||
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
|
||||
}
|
||||
// get function
|
||||
return strings.Join(strings.FieldsFunc(name, fargs), "-")
|
||||
}
|
||||
|
||||
170
provider/provider_test.go
Normal file
@@ -0,0 +1,170 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type myProvider struct {
|
||||
BaseProvider
|
||||
}
|
||||
|
||||
func (p *myProvider) Foo() string {
|
||||
return "bar"
|
||||
}
|
||||
|
||||
func TestConfigurationErrors(t *testing.T) {
|
||||
templateErrorFile, err := ioutil.TempFile("", "provider-configuration-error")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(templateErrorFile.Name())
|
||||
data := []byte("Not a valid template {{ Bar }}")
|
||||
err = ioutil.WriteFile(templateErrorFile.Name(), data, 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
templateInvalidTOMLFile, err := ioutil.TempFile("", "provider-configuration-error")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(templateInvalidTOMLFile.Name())
|
||||
data = []byte(`Hello {{ .Name }}
|
||||
{{ Foo }}`)
|
||||
err = ioutil.WriteFile(templateInvalidTOMLFile.Name(), data, 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
invalids := []struct {
|
||||
provider *myProvider
|
||||
defaultTemplate string
|
||||
expectedError string
|
||||
funcMap template.FuncMap
|
||||
templateObjects interface{}
|
||||
}{
|
||||
{
|
||||
provider: &myProvider{
|
||||
BaseProvider{
|
||||
Filename: "/non/existent/template.tmpl",
|
||||
},
|
||||
},
|
||||
expectedError: "open /non/existent/template.tmpl: no such file or directory",
|
||||
},
|
||||
{
|
||||
provider: &myProvider{},
|
||||
defaultTemplate: "non/existent/template.tmpl",
|
||||
expectedError: "Asset non/existent/template.tmpl not found",
|
||||
},
|
||||
{
|
||||
provider: &myProvider{
|
||||
BaseProvider{
|
||||
Filename: templateErrorFile.Name(),
|
||||
},
|
||||
},
|
||||
expectedError: `function "Bar" not defined`,
|
||||
},
|
||||
{
|
||||
provider: &myProvider{
|
||||
BaseProvider{
|
||||
Filename: templateInvalidTOMLFile.Name(),
|
||||
},
|
||||
},
|
||||
expectedError: "Near line 1, key 'Hello': Near line 1: Expected key separator '=', but got '<' instead",
|
||||
funcMap: template.FuncMap{
|
||||
"Foo": func() string {
|
||||
return "bar"
|
||||
},
|
||||
},
|
||||
templateObjects: struct{ Name string }{Name: "bar"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, invalid := range invalids {
|
||||
configuration, err := invalid.provider.getConfiguration(invalid.defaultTemplate, invalid.funcMap, nil)
|
||||
if err == nil || !strings.Contains(err.Error(), invalid.expectedError) {
|
||||
t.Fatalf("should have generate an error with %q, got %v", invalid.expectedError, err)
|
||||
}
|
||||
if configuration != nil {
|
||||
t.Fatalf("shouldn't have return a configuration object : %v", configuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfiguration(t *testing.T) {
|
||||
templateFile, err := ioutil.TempFile("", "provider-configuration")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(templateFile.Name())
|
||||
data := []byte(`[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.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
|
||||
|
||||
[frontends]
|
||||
[frontends.frontend1]
|
||||
backend = "backend1"
|
||||
passHostHeader = true
|
||||
[frontends.frontend11.routes.test_2]
|
||||
rule = "Path"
|
||||
value = "/test"`)
|
||||
err = ioutil.WriteFile(templateFile.Name(), data, 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
provider := &myProvider{
|
||||
BaseProvider{
|
||||
Filename: templateFile.Name(),
|
||||
},
|
||||
}
|
||||
configuration, err := provider.getConfiguration(templateFile.Name(), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Shouldn't have error out, got %v", err)
|
||||
}
|
||||
if configuration == nil {
|
||||
t.Fatalf("Configuration should not be nil, but was")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplace(t *testing.T) {
|
||||
cases := []struct {
|
||||
str string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
str: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
str: "foo",
|
||||
expected: "bar",
|
||||
},
|
||||
{
|
||||
str: "foo foo",
|
||||
expected: "bar bar",
|
||||
},
|
||||
{
|
||||
str: "somethingfoo",
|
||||
expected: "somethingbar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
actual := replace("foo", "bar", c.str)
|
||||
if actual != c.expected {
|
||||
t.Fatalf("expected %q, got %q, for %q", c.expected, actual, c.str)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,20 @@
|
||||
package provider
|
||||
|
||||
import "github.com/emilevauge/traefik/types"
|
||||
import (
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/zookeeper"
|
||||
)
|
||||
|
||||
// Zookepper holds configurations of the Zookepper provider.
|
||||
type Zookepper struct {
|
||||
Watch bool
|
||||
Endpoint string
|
||||
Prefix string
|
||||
Filename string
|
||||
KvProvider *Kv
|
||||
Kv
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Zookepper) Provide(configurationChan chan<- types.ConfigMessage) error {
|
||||
provider.KvProvider = NewZkProvider(provider)
|
||||
return provider.KvProvider.provide(configurationChan)
|
||||
provider.storeType = store.ZK
|
||||
zookeeper.Register()
|
||||
return provider.provide(configurationChan)
|
||||
}
|
||||
|
||||
147
rules.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/gorilla/mux"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Rules holds rule parsing and configuration
|
||||
type Rules struct {
|
||||
route *serverRoute
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *Rules) host(hosts ...string) *mux.Route {
|
||||
return r.route.route.MatcherFunc(func(req *http.Request, route *mux.RouteMatch) bool {
|
||||
reqHost, _, err := net.SplitHostPort(req.Host)
|
||||
if err != nil {
|
||||
reqHost = req.Host
|
||||
}
|
||||
for _, host := range hosts {
|
||||
if reqHost == strings.TrimSpace(host) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Rules) hostRegexp(hosts ...string) *mux.Route {
|
||||
router := r.route.route.Subrouter()
|
||||
for _, host := range hosts {
|
||||
router.Host(strings.TrimSpace(host))
|
||||
}
|
||||
return r.route.route
|
||||
}
|
||||
|
||||
func (r *Rules) path(paths ...string) *mux.Route {
|
||||
router := r.route.route.Subrouter()
|
||||
for _, path := range paths {
|
||||
router.Path(strings.TrimSpace(path))
|
||||
}
|
||||
return r.route.route
|
||||
}
|
||||
|
||||
func (r *Rules) pathPrefix(paths ...string) *mux.Route {
|
||||
router := r.route.route.Subrouter()
|
||||
for _, path := range paths {
|
||||
router.PathPrefix(strings.TrimSpace(path))
|
||||
}
|
||||
return r.route.route
|
||||
}
|
||||
|
||||
type bySize []string
|
||||
|
||||
func (a bySize) Len() int { return len(a) }
|
||||
func (a bySize) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a bySize) Less(i, j int) bool { return len(a[i]) > len(a[j]) }
|
||||
|
||||
func (r *Rules) pathStrip(paths ...string) *mux.Route {
|
||||
sort.Sort(bySize(paths))
|
||||
r.route.stripPrefixes = paths
|
||||
router := r.route.route.Subrouter()
|
||||
for _, path := range paths {
|
||||
router.Path(strings.TrimSpace(path))
|
||||
}
|
||||
return r.route.route
|
||||
}
|
||||
|
||||
func (r *Rules) pathPrefixStrip(paths ...string) *mux.Route {
|
||||
sort.Sort(bySize(paths))
|
||||
r.route.stripPrefixes = paths
|
||||
router := r.route.route.Subrouter()
|
||||
for _, path := range paths {
|
||||
router.PathPrefix(strings.TrimSpace(path))
|
||||
}
|
||||
return r.route.route
|
||||
}
|
||||
|
||||
func (r *Rules) methods(methods ...string) *mux.Route {
|
||||
return r.route.route.Methods(methods...)
|
||||
}
|
||||
|
||||
func (r *Rules) headers(headers ...string) *mux.Route {
|
||||
return r.route.route.Headers(headers...)
|
||||
}
|
||||
|
||||
func (r *Rules) headersRegexp(headers ...string) *mux.Route {
|
||||
return r.route.route.HeadersRegexp(headers...)
|
||||
}
|
||||
|
||||
// Parse parses rules expressions
|
||||
func (r *Rules) Parse(expression string) (*mux.Route, error) {
|
||||
functions := map[string]interface{}{
|
||||
"Host": r.host,
|
||||
"HostRegexp": r.hostRegexp,
|
||||
"Path": r.path,
|
||||
"PathStrip": r.pathStrip,
|
||||
"PathPrefix": r.pathPrefix,
|
||||
"PathPrefixStrip": r.pathPrefixStrip,
|
||||
"Method": r.methods,
|
||||
"Headers": r.headers,
|
||||
"HeadersRegexp": r.headersRegexp,
|
||||
}
|
||||
f := func(c rune) bool {
|
||||
return c == ':'
|
||||
}
|
||||
// get function
|
||||
parsedFunctions := strings.FieldsFunc(expression, f)
|
||||
if len(parsedFunctions) == 0 {
|
||||
return nil, errors.New("Error parsing rule: " + expression)
|
||||
}
|
||||
parsedFunction, ok := functions[parsedFunctions[0]]
|
||||
if !ok {
|
||||
return nil, errors.New("Error parsing rule: " + expression + ". Unknow function: " + parsedFunctions[0])
|
||||
}
|
||||
parsedFunctions = append(parsedFunctions[:0], parsedFunctions[1:]...)
|
||||
fargs := func(c rune) bool {
|
||||
return c == ',' || c == ';'
|
||||
}
|
||||
// get function
|
||||
parsedArgs := strings.FieldsFunc(strings.Join(parsedFunctions, ":"), fargs)
|
||||
if len(parsedArgs) == 0 {
|
||||
return nil, errors.New("Error parsing args from rule: " + expression)
|
||||
}
|
||||
|
||||
inputs := make([]reflect.Value, len(parsedArgs))
|
||||
for i := range parsedArgs {
|
||||
inputs[i] = reflect.ValueOf(parsedArgs[i])
|
||||
}
|
||||
method := reflect.ValueOf(parsedFunction)
|
||||
if method.IsValid() {
|
||||
resultRoute := method.Call(inputs)[0].Interface().(*mux.Route)
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if resultRoute.GetError() != nil {
|
||||
return nil, resultRoute.GetError()
|
||||
}
|
||||
return resultRoute, nil
|
||||
}
|
||||
return nil, errors.New("Method not found: " + parsedFunctions[0])
|
||||
}
|
||||
28
safe/safe.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package safe
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
// Go starts a recoverable goroutine
|
||||
func Go(goroutine func()) {
|
||||
GoWithRecover(goroutine, defaultRecoverGoroutine)
|
||||
}
|
||||
|
||||
// GoWithRecover starts a recoverable goroutine using given customRecover() function
|
||||
func GoWithRecover(goroutine func(), customRecover func(err interface{})) {
|
||||
go func() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
customRecover(err)
|
||||
}
|
||||
}()
|
||||
goroutine()
|
||||
}()
|
||||
}
|
||||
|
||||
func defaultRecoverGoroutine(err interface{}) {
|
||||
log.Println(err)
|
||||
debug.PrintStack()
|
||||
}
|
||||
@@ -3,23 +3,24 @@
|
||||
if [ -z "$VALIDATE_UPSTREAM" ]; then
|
||||
# this is kind of an expensive check, so let's not do this twice if we
|
||||
# are running more than one validate bundlescript
|
||||
|
||||
VALIDATE_REPO='https://github.com/emilevauge/traefik.git'
|
||||
|
||||
VALIDATE_REPO='https://github.com/containous/traefik.git'
|
||||
VALIDATE_BRANCH='master'
|
||||
|
||||
if [ "$TRAVIS" = 'true' -a "$TRAVIS_PULL_REQUEST" != 'false' ]; then
|
||||
VALIDATE_REPO="https://github.com/${TRAVIS_REPO_SLUG}.git"
|
||||
VALIDATE_BRANCH="${TRAVIS_BRANCH}"
|
||||
fi
|
||||
|
||||
|
||||
# Should not be needed for now O:)
|
||||
# if [ "$TRAVIS" = 'true' -a "$TRAVIS_PULL_REQUEST" != 'false' ]; then
|
||||
# VALIDATE_REPO="https://github.com/${TRAVIS_REPO_SLUG}.git"
|
||||
# VALIDATE_BRANCH="${TRAVIS_BRANCH}"
|
||||
# fi
|
||||
|
||||
VALIDATE_HEAD="$(git rev-parse --verify HEAD)"
|
||||
|
||||
|
||||
git fetch -q "$VALIDATE_REPO" "refs/heads/$VALIDATE_BRANCH"
|
||||
VALIDATE_UPSTREAM="$(git rev-parse --verify FETCH_HEAD)"
|
||||
|
||||
|
||||
VALIDATE_COMMIT_LOG="$VALIDATE_UPSTREAM..$VALIDATE_HEAD"
|
||||
VALIDATE_COMMIT_DIFF="$VALIDATE_UPSTREAM...$VALIDATE_HEAD"
|
||||
|
||||
|
||||
validate_diff() {
|
||||
if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then
|
||||
git diff "$VALIDATE_COMMIT_DIFF" "$@"
|
||||
|
||||
@@ -8,6 +8,11 @@ fi
|
||||
|
||||
rm -f dist/traefik
|
||||
|
||||
FLAGS=""
|
||||
if [ -n "$VERBOSE" ]; then
|
||||
FLAGS="${FLAGS} -v"
|
||||
fi
|
||||
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION=$(git rev-parse HEAD)
|
||||
fi
|
||||
@@ -17,4 +22,4 @@ if [ -z "$DATE" ]; then
|
||||
fi
|
||||
|
||||
# Build binaries
|
||||
CGO_ENABLED=0 go build -ldflags "-X main.Version=$VERSION -X main.BuildDate=$DATE" -a -installsuffix nocgo -o dist/traefik .
|
||||
CGO_ENABLED=0 GOGC=off go build $FLAGS -ldflags "-X main.Version=$VERSION -X main.BuildDate=$DATE" -a -installsuffix nocgo -o dist/traefik .
|
||||
|
||||
@@ -32,5 +32,5 @@ fi
|
||||
rm -f dist/traefik_*
|
||||
|
||||
# Build binaries
|
||||
gox -ldflags "-X main.Version=$VERSION -X main.BuildDate=$DATE" "${OS_PLATFORM_ARG[@]}" "${OS_ARCH_ARG[@]}" \
|
||||
GOGC=off gox -ldflags "-X main.Version=$VERSION -X main.BuildDate=$DATE" "${OS_PLATFORM_ARG[@]}" "${OS_ARCH_ARG[@]}" \
|
||||
-output="dist/traefik_{{.OS}}-{{.Arch}}"
|
||||
|
||||
72
script/deploy.sh
Executable file
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
if ([ "$TRAVIS_BRANCH" = "master" ] || [ ! -z "$TRAVIS_TAG" ]) && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
|
||||
echo "Deploying..."
|
||||
else
|
||||
echo "Skipping deploy"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config --global user.email "emile@vauge.com"
|
||||
git config --global user.name "Emile Vauge"
|
||||
|
||||
# load ssh key
|
||||
echo "Loading key..."
|
||||
openssl aes-256-cbc -d -k "$pass" -in .travis/traefik.id_rsa.enc -out ~/.ssh/traefik.id_rsa
|
||||
eval "$(ssh-agent -s)"
|
||||
chmod 600 ~/.ssh/traefik.id_rsa
|
||||
ssh-add ~/.ssh/traefik.id_rsa
|
||||
|
||||
# download github release
|
||||
echo "Downloading ghr..."
|
||||
curl -LOs https://github.com/tcnksm/ghr/releases/download/pre-release/linux_amd64.zip
|
||||
unzip -q linux_amd64.zip
|
||||
sudo mv ghr /usr/bin/ghr
|
||||
sudo chmod +x /usr/bin/ghr
|
||||
|
||||
# github release and tag
|
||||
echo "Github release..."
|
||||
ghr -t $GITHUB_TOKEN -u containous -r traefik --prerelease ${VERSION} dist/
|
||||
|
||||
# update docs.traefik.io
|
||||
echo "Generating and updating documentation..."
|
||||
# DOESN'T WORK :'(
|
||||
# git remote add ssh git@github.com:containous/traefik.git
|
||||
# mkdocs gh-deploy -m $VERSION -c -r ssh
|
||||
|
||||
mkdir site
|
||||
cd site
|
||||
git init
|
||||
git remote add origin git@github.com:containous/traefik.git
|
||||
git fetch origin
|
||||
git checkout gh-pages
|
||||
cd ..
|
||||
mkdocs build --clean
|
||||
cd site
|
||||
git add .
|
||||
echo $VERSION | git commit --file -
|
||||
git push -q -f origin gh-pages > /dev/null 2>&1
|
||||
|
||||
# update traefik-library-image repo (official Docker image)
|
||||
echo "Updating traefik-library-imag repo..."
|
||||
git clone git@github.com:containous/traefik-library-image.git
|
||||
cd traefik-library-image
|
||||
./update.sh $VERSION
|
||||
git add -A
|
||||
echo $VERSION | git commit --file -
|
||||
echo $VERSION | git tag -a $VERSION --file -
|
||||
git push -q --follow-tags -u origin master > /dev/null 2>&1
|
||||
|
||||
# create docker image emilevauge/traefik (compatibility)
|
||||
echo "Updating docker emilevauge/traefik image..."
|
||||
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
|
||||
docker tag containous/traefik emilevauge/traefik:latest
|
||||
docker push emilevauge/traefik:latest
|
||||
docker tag emilevauge/traefik:latest emilevauge/traefik:${VERSION}
|
||||
docker push emilevauge/traefik:${VERSION}
|
||||
|
||||
cd ..
|
||||
rm -Rf traefik-library-image/
|
||||
|
||||
echo "Deployed"
|
||||
@@ -1,10 +1,14 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
export SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
export DEST=.
|
||||
|
||||
TESTFLAGS="$TESTFLAGS -test.timeout=30m -check.v"
|
||||
TESTFLAGS="${TESTFLAGS} -test.timeout=30m -check.v"
|
||||
|
||||
if [ -n "$VERBOSE" ]; then
|
||||
TESTFLAGS="${TESTFLAGS} -v"
|
||||
fi
|
||||
|
||||
cd integration
|
||||
go test $TESTFLAGS
|
||||
|
||||
CGO_ENABLED=0 go test $TESTFLAGS
|
||||
|
||||