mirror of
https://github.com/containous/traefik.git
synced 2025-09-07 09:44:23 +03:00
Compare commits
257 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
cab746bcca | ||
|
15e9426e19 | ||
|
cf31e02b9c | ||
|
2ac217c741 | ||
|
bc875d6268 | ||
|
bbb8d922fc | ||
|
37b44cc706 | ||
|
3c6e0b3e68 | ||
|
e21feae561 | ||
|
c766439fed | ||
|
0ecfbb8279 | ||
|
82d631572c | ||
|
fa4d2d325d | ||
|
c469bbb70c | ||
|
b3e6c7f598 | ||
|
40afd641a9 | ||
|
fba3db5291 | ||
|
023cda1398 | ||
|
3cf6d7e9e5 | ||
|
0d657a09b0 | ||
|
07176396e2 | ||
|
5183c98fb7 | ||
|
5a57515c6b | ||
|
3490b9d35d | ||
|
dd0a20a668 | ||
|
d13cef6ff6 | ||
|
9f149977d6 | ||
|
e65544f414 | ||
|
8010758b29 | ||
|
75d92c6967 | ||
|
0f67cc7818 | ||
|
f428a752a5 | ||
|
3fe3784b6c | ||
|
e4d63331cf | ||
|
8ae521db64 | ||
|
24fde36b20 | ||
|
7c55a4fd0c | ||
|
7b1c0a97f7 | ||
|
8392846bd4 | ||
|
07db6a2df1 | ||
|
cc9bb4b1f8 | ||
|
de91b99639 | ||
|
c582ea5ff0 | ||
|
b5de37e722 | ||
|
ee9032f0bf | ||
|
11a68ce7f9 | ||
|
0dbac0af0d | ||
|
9541ee4cf6 | ||
|
2958a67ce5 | ||
|
eebbf6ebbb | ||
|
0133178b84 | ||
|
9af5ba34ae | ||
|
9b24e13418 | ||
|
8fd880a3f1 | ||
|
89700666b9 | ||
|
343e0547df | ||
|
a7d5e6ce4f | ||
|
d342ae68d8 | ||
|
a87cd3fc2c | ||
|
28a4d65b38 | ||
|
253dbfab94 | ||
|
727f79f432 | ||
|
6a56eb480b | ||
|
155d0900bb | ||
|
a59a165cd7 | ||
|
5d540be81b | ||
|
05f2449d84 | ||
|
44fa364cdd | ||
|
04a25b841f | ||
|
efbbff671c | ||
|
27c2c721ed | ||
|
abe16d4480 | ||
|
445bb8189b | ||
|
ff7dfdcd43 | ||
|
5e662c9dbd | ||
|
78d60b3651 | ||
|
7a7992a639 | ||
|
39f8f6868a | ||
|
556915cab6 | ||
|
bff654b843 | ||
|
3a875e2954 | ||
|
bdb63ac785 | ||
|
9a5dc54f85 | ||
|
48524a58ff | ||
|
38bd49b97e | ||
|
28054a0be3 | ||
|
250a0863f6 | ||
|
b1764a6864 | ||
|
41f8f0113b | ||
|
db63e84a9f | ||
|
e0a4c58081 | ||
|
d2b47a5681 | ||
|
106e5c1f92 | ||
|
c00a9fae0c | ||
|
087bbd2e3e | ||
|
e16f2bb23d | ||
|
8d0bacf146 | ||
|
354f69b2f6 | ||
|
39e6b16069 | ||
|
b30272d896 | ||
|
755822bf14 | ||
|
99ffc26d40 | ||
|
4a8f032304 | ||
|
a0b775a7c0 | ||
|
0ab0bdf818 | ||
|
fce32ea5c7 | ||
|
8d3c77a0b9 | ||
|
00de73bdfc | ||
|
96197af3f1 | ||
|
dacde21c27 | ||
|
0d3b2ed230 | ||
|
fa4226c742 | ||
|
7cb4c42772 | ||
|
99f251451e | ||
|
d5f9a80b6c | ||
|
d324040adc | ||
|
da5eba17d8 | ||
|
434596b103 | ||
|
71a185c70e | ||
|
cbbb5f4ccb | ||
|
89ec25f718 | ||
|
e5b688214c | ||
|
225dbcce0a | ||
|
b22dc213e8 | ||
|
ad12a7264e | ||
|
29059b77a8 | ||
|
cdaa64a4b2 | ||
|
bc4296729f | ||
|
3a3630f3ef | ||
|
93ce747205 | ||
|
1493a4c815 | ||
|
54be6beaab | ||
|
e9fc9fdf12 | ||
|
ba4670eddc | ||
|
5a67d0ac84 | ||
|
be362f0d9f | ||
|
a394e6a3e3 | ||
|
1a5f1977c4 | ||
|
feee8ad72e | ||
|
c9e78c4f4a | ||
|
d0e2349dfd | ||
|
d516cbfe6c | ||
|
86fd5b4c97 | ||
|
1131a972cd | ||
|
2048f77178 | ||
|
a70c6f25ea | ||
|
490427f94d | ||
|
7cc91a8244 | ||
|
4f951a242b | ||
|
c095fc1eab | ||
|
c1182377db | ||
|
02473328e7 | ||
|
2b00cdf330 | ||
|
18cf49755e | ||
|
3a7de0be5c | ||
|
a1b610ee03 | ||
|
4d99b84e5b | ||
|
e20d13c44e | ||
|
18e9064d25 | ||
|
fad3038df2 | ||
|
8e4c4f8407 | ||
|
68bd24d065 | ||
|
d15a17b634 | ||
|
fa1090b6eb | ||
|
483ef486af | ||
|
175659a3dd | ||
|
dd85cbca39 | ||
|
22b97b7214 | ||
|
db68dd3bc1 | ||
|
85b9c19871 | ||
|
2bfc237e53 | ||
|
d74ea22d7d | ||
|
8004132a3a | ||
|
a6f4183cde | ||
|
51e9f3ede2 | ||
|
bfc7b3d183 | ||
|
8a348423ae | ||
|
e4952cd145 | ||
|
5b0bf5d150 | ||
|
79180dc021 | ||
|
599c95e5f6 | ||
|
e1ed8b71f6 | ||
|
6ca142bf20 | ||
|
6b20d2a5f3 | ||
|
bef55db120 | ||
|
3bb3658d7d | ||
|
a4034ce1e2 | ||
|
d9fc66fdbc | ||
|
3ebfd729cf | ||
|
6adb346cee | ||
|
318ff52ff3 | ||
|
b7b0f8f68d | ||
|
94bb7a1435 | ||
|
913a297e8d | ||
|
d469d426f8 | ||
|
ec05fbcf19 | ||
|
686faf0556 | ||
|
fe2d4e0d38 | ||
|
c500873586 | ||
|
fc788eb426 | ||
|
87eac1dc1a | ||
|
91d9b9811f | ||
|
71beb4b08f | ||
|
d26f06e2d1 | ||
|
dca08af003 | ||
|
4c740e26d7 | ||
|
131f581f77 | ||
|
9236a43a4d | ||
|
7f4eddf6d6 | ||
|
d1e631a487 | ||
|
0b78375211 | ||
|
15540764a0 | ||
|
82234cbbb2 | ||
|
22392daef7 | ||
|
567387aee0 | ||
|
5b71e3184a | ||
|
e1724444ac | ||
|
cf8940e80e | ||
|
15732269da | ||
|
7b06be8f5e | ||
|
d2dcec40e1 | ||
|
2af6cc4d1b | ||
|
56c6174d61 | ||
|
66e914a8ab | ||
|
8ae9607d9b | ||
|
5c0297fb61 | ||
|
f5bf9a2cda | ||
|
987ab7612d | ||
|
a186d5f87a | ||
|
874ea62dd5 | ||
|
f0b991e1a8 | ||
|
adf385fdf3 | ||
|
7af6bc093d | ||
|
3708fa864b | ||
|
28276e1b37 | ||
|
b0efd685a9 | ||
|
422aacf8e6 | ||
|
e068ee09ca | ||
|
91e3bdff48 | ||
|
4299d1526b | ||
|
8d9caaec71 | ||
|
91634d5c1c | ||
|
f5463c3d38 | ||
|
73b70393d4 | ||
|
d174ed75c7 | ||
|
513d261f10 | ||
|
acf425b6cf | ||
|
98b35affd5 | ||
|
b3cc1e1af1 | ||
|
2b770ae2f8 | ||
|
952fcf5d09 | ||
|
931a124349 | ||
|
ab52f4d91d | ||
|
f3182ef29b | ||
|
05f6b79e29 | ||
|
14db2343c9 | ||
|
67eb0c8de0 |
11
.github/CONTRIBUTING.md
vendored
11
.github/CONTRIBUTING.md
vendored
@@ -12,7 +12,7 @@ You need to run the `binary` target. This will create binaries for Linux platfor
|
||||
$ 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
|
||||
Step 0 : FROM golang:1.7
|
||||
---> 8c6473912976
|
||||
Step 1 : RUN go get github.com/Masterminds/glide
|
||||
[...]
|
||||
@@ -30,8 +30,7 @@ traefik*
|
||||
|
||||
###### Setting up your `go` environment
|
||||
|
||||
- You need `go` v1.5+ (1.7 is acceptable)
|
||||
- You need to set `$ export GO15VENDOREXPERIMENT=1` environment variable if you are using go v1.5 (it is already enabled in 1.6+)
|
||||
- You need `go` v1.7+
|
||||
- It is recommended you clone Træfɪk into a directory like `~/go/src/github.com/containous/traefik` (This is the official golang workspace hierarchy, and will allow dependencies to resolve properly)
|
||||
- This will allow your `GOPATH` and `PATH` variable to be set to `~/go` via:
|
||||
```
|
||||
@@ -49,14 +48,14 @@ This can be verified via `$ go env`
|
||||
|
||||
The idea behind `glide` is the following :
|
||||
|
||||
- when checkout(ing) a project, run `$ glide install` from the cloned directory to install
|
||||
- when checkout(ing) a project, run `$ glide install -v` from the cloned directory to install
|
||||
(`go get …`) the dependencies in your `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
|
||||
$ glide install --strip-vendor
|
||||
# generate (Only required to integrate other components such as web dashboard)
|
||||
$ go generate
|
||||
# Standard go build
|
||||
@@ -112,6 +111,8 @@ More: https://labix.org/gocheck
|
||||
```
|
||||
ok _/home/vincent/src/github/vdemeester/traefik 0.004s
|
||||
```
|
||||
- Note that `$ go test ./...` will run all tests (including the ones in the vendor directory for the dependencies that glide have fetched). If you only want to run the tests for traefik use `$ go test $(glide novendor)` instead.
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
|
13
.github/ISSUE_TEMPLATE
vendored
Normal file
13
.github/ISSUE_TEMPLATE
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
### What version of Traefik are you using (`traefik version`)?
|
||||
|
||||
|
||||
### What is your environment & configuration (arguments, toml...)?
|
||||
|
||||
|
||||
### What did you do?
|
||||
|
||||
|
||||
### What did you expect to see?
|
||||
|
||||
|
||||
### What did you see instead?
|
26
.github/cpr.sh
vendored
Executable file
26
.github/cpr.sh
vendored
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# git config --global alias.cpr '!sh .github/cpr.sh'
|
||||
|
||||
set -e # stop on error
|
||||
|
||||
usage="$(basename "$0") pr -- Checkout a Pull Request locally"
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Illegal number of parameters"
|
||||
echo "$usage" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
command -v jq >/dev/null 2>&1 || { echo "I require jq but it's not installed. Aborting." >&2; exit 1; }
|
||||
|
||||
set -x # echo on
|
||||
|
||||
initial=$(git rev-parse --abbrev-ref HEAD)
|
||||
pr=$1
|
||||
remote=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.repo.owner.login)
|
||||
branch=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.ref)
|
||||
|
||||
git remote add $remote git@github.com:$remote/traefik.git
|
||||
git fetch $remote $branch
|
||||
git checkout -t $remote/$branch
|
27
.github/rmpr.sh
vendored
Executable file
27
.github/rmpr.sh
vendored
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# git config --global alias.rmpr '!sh .github/rmpr.sh'
|
||||
|
||||
set -e # stop on error
|
||||
|
||||
usage="$(basename "$0") pr -- remove a Pull Request local branch & remote"
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Illegal number of parameters"
|
||||
echo "$usage" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
command -v jq >/dev/null 2>&1 || { echo "I require jq but it's not installed. Aborting." >&2; exit 1; }
|
||||
|
||||
set -x # echo on
|
||||
|
||||
initial=$(git rev-parse --abbrev-ref HEAD)
|
||||
pr=$1
|
||||
remote=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.repo.owner.login)
|
||||
branch=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.ref)
|
||||
|
||||
# clean
|
||||
git checkout $initial
|
||||
git branch -D $branch
|
||||
git remote remove $remote
|
36
.github/rpr.sh
vendored
Executable file
36
.github/rpr.sh
vendored
Executable file
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# git config --global alias.rpr '!sh .github/rpr.sh'
|
||||
|
||||
set -e # stop on error
|
||||
|
||||
usage="$(basename "$0") pr remote/branch -- rebase a Pull Request against a remote branch"
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Illegal number of parameters"
|
||||
echo "$usage" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
command -v jq >/dev/null 2>&1 || { echo "I require jq but it's not installed. Aborting." >&2; exit 1; }
|
||||
|
||||
set -x # echo on
|
||||
|
||||
initial=$(git rev-parse --abbrev-ref HEAD)
|
||||
pr=$1
|
||||
base=$2
|
||||
remote=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.repo.owner.login)
|
||||
branch=$(curl -s https://api.github.com/repos/containous/traefik/pulls/$pr | jq -r .head.ref)
|
||||
|
||||
clean ()
|
||||
{
|
||||
git checkout $initial
|
||||
.github/rmpr.sh $pr
|
||||
}
|
||||
|
||||
trap clean EXIT
|
||||
|
||||
.github/cpr.sh $pr
|
||||
|
||||
git rebase $base
|
||||
git push -f $remote $branch
|
83
.travis.yml
83
.travis.yml
@@ -1,34 +1,67 @@
|
||||
branches:
|
||||
sudo: required
|
||||
dist: trusty
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
env:
|
||||
global:
|
||||
- secure: btt4r13t09gQlHb6gYrvGC2yGCMMHfnp1Mz1RQedc4Mpf/FfT8aE6xmK2a2i9CCvskjrP0t/BFaS4yxIURjnFRn+ugQIEa0pLspB9UJArW/vgOSpIWM9/OQ/fg8z5XuMxN6Md4DL1/iLypMNSageA1x0TRdt89+D1N1dALpg5XRCXLFbC84TLi0gjlFuib9ibPKzEhLT+anCRJ6iZMzeupDSoaCVbAtJMoDvXw4+4AcRZ1+k4MybBLyCib5boaEOt4pTT88mz4Kk0YaMwPVJyg9Qv36VqyUcPS09Yd95LuyVQ4+tZt8Y1ccbIzULsK+sLM3hLCzxlmlpN3dQBlZJiiRtQde0mgGAKyC0P0A1XjuDTywcsa5edB+fTk1Dsewz9xZ9V0NmMz8t+UNZnaSsAPga9i86jULbXUUwMVSzVRc+Xgx02liB/8qI1xYC9FM6ilStt7rn7mF0k3KbiWhcptgeXjO6Lah9FjEKd5w4MXsdUSTi/86rQaLo+kj+XdaTrXCTulKHyRyQEUj+8V1w0oVz7pcGjePHd7y5oU9ByifVQy6sytuFBfRZvugM5bKHo+i0pcWvixrZS42DrzwxZJsspANOvqSe5ifVbvOkfUppQdCBIwptxV5N1b49XPKU3W/w34QJ8xGmKp3TFA7WwVCztriFHjPgiRpB3EG99Bg=
|
||||
- REPO: $TRAVIS_REPO_SLUG
|
||||
- VERSION: $TRAVIS_TAG
|
||||
- CODENAME: camembert
|
||||
matrix:
|
||||
- DOCKER_VERSION=1.9.1
|
||||
- DOCKER_VERSION=1.10.1
|
||||
sudo: required
|
||||
services:
|
||||
- docker
|
||||
- CODENAME: morbier
|
||||
|
||||
matrix:
|
||||
fast_finish: true
|
||||
include:
|
||||
- env: DOCKER_VERSION=1.10.3
|
||||
- env: DOCKER_VERSION=1.12.6
|
||||
|
||||
before_install:
|
||||
- sudo -E apt-get -yq update
|
||||
- sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-engine=${DOCKER_VERSION}*
|
||||
install:
|
||||
- sudo service docker stop
|
||||
- sudo curl https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION} -o /usr/bin/docker
|
||||
- sudo chmod +x /usr/bin/docker
|
||||
- sudo service docker start
|
||||
- sleep 5
|
||||
- docker version
|
||||
- pip install --user mkdocs
|
||||
- pip install --user pymdown-extensions
|
||||
- pip install --user mkdocs-bootswatch
|
||||
- docker version
|
||||
- pip install --user -r requirements.txt
|
||||
before_script:
|
||||
- make validate
|
||||
- make binary
|
||||
- make validate
|
||||
- make binary
|
||||
script:
|
||||
- make test-unit
|
||||
- make test-integration
|
||||
- make crossbinary
|
||||
- make image
|
||||
- travis_retry make test-unit
|
||||
- travis_retry make test-integration
|
||||
after_failure:
|
||||
- docker ps
|
||||
after_success:
|
||||
- make deploy
|
||||
- make deploy-pr
|
||||
- make crossbinary
|
||||
- make image
|
||||
before_deploy:
|
||||
- mkdocs build --clean
|
||||
- tar cfz dist/traefik-${VERSION}.src.tar.gz --exclude-vcs --exclude dist .
|
||||
deploy:
|
||||
- provider: pages
|
||||
edge: true
|
||||
github_token: ${GITHUB_TOKEN}
|
||||
local_dir: site
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: containous/traefik
|
||||
tags: true
|
||||
- provider: releases
|
||||
api_key: ${GITHUB_TOKEN}
|
||||
file: dist/traefik*
|
||||
skip_cleanup: true
|
||||
file_glob: true
|
||||
on:
|
||||
repo: containous/traefik
|
||||
tags: true
|
||||
- provider: script
|
||||
script: sh script/deploy.sh
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: containous/traefik
|
||||
tags: true
|
||||
- provider: script
|
||||
script: sh script/deploy-docker.sh
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: containous/traefik
|
||||
|
284
CHANGELOG.md
284
CHANGELOG.md
@@ -1,5 +1,289 @@
|
||||
# Change Log
|
||||
|
||||
## [v1.2.3](https://github.com/containous/traefik/tree/v1.2.3) (2017-04-13)
|
||||
[Full Changelog](https://github.com/containous/traefik/compare/v1.2.2...v1.2.3)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix too many redirect [\#1433](https://github.com/containous/traefik/pull/1433) ([emilevauge](https://github.com/emilevauge))
|
||||
|
||||
## [v1.2.2](https://github.com/containous/traefik/tree/v1.2.2) (2017-04-11)
|
||||
[Full Changelog](https://github.com/containous/traefik/compare/v1.2.1...v1.2.2)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Carry PR 1271 [\#1417](https://github.com/containous/traefik/pull/1417) ([emilevauge](https://github.com/emilevauge))
|
||||
- Fix postloadconfig acme & Docker filter empty rule [\#1401](https://github.com/containous/traefik/pull/1401) ([emilevauge](https://github.com/emilevauge))
|
||||
|
||||
## [v1.2.1](https://github.com/containous/traefik/tree/v1.2.1) (2017-03-27)
|
||||
[Full Changelog](https://github.com/containous/traefik/compare/v1.2.0...v1.2.1)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- bump lego 0e2937900 [\#1347](https://github.com/containous/traefik/pull/1347) ([emilevauge](https://github.com/emilevauge))
|
||||
- k8s: Do not log service fields when GetService is failing. [\#1331](https://github.com/containous/traefik/pull/1331) ([timoreimann](https://github.com/timoreimann))
|
||||
|
||||
## [v1.2.0](https://github.com/containous/traefik/tree/v1.2.0) (2017-03-20)
|
||||
[Full Changelog](https://github.com/containous/traefik/compare/v1.1.2...v1.2.0)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Docker: Added warning if network could not be found [\#1310](https://github.com/containous/traefik/pull/1310) ([zweizeichen](https://github.com/zweizeichen))
|
||||
- Add filter on task status in addition to desired status \(Docker Provider - swarm\) [\#1304](https://github.com/containous/traefik/pull/1304) ([Yshayy](https://github.com/Yshayy))
|
||||
- Abort Kubernetes Ingress update if Kubernetes API call fails [\#1295](https://github.com/containous/traefik/pull/1295) ([Regner](https://github.com/Regner))
|
||||
- Small fixes [\#1291](https://github.com/containous/traefik/pull/1291) ([emilevauge](https://github.com/emilevauge))
|
||||
- Rename health check URL parameter to path. [\#1285](https://github.com/containous/traefik/pull/1285) ([timoreimann](https://github.com/timoreimann))
|
||||
- Update Oxy, fix for \#1199 [\#1278](https://github.com/containous/traefik/pull/1278) ([akanto](https://github.com/akanto))
|
||||
- Fix metrics registering [\#1258](https://github.com/containous/traefik/pull/1258) ([matevzmihalic](https://github.com/matevzmihalic))
|
||||
- Update DefaultMaxIdleConnsPerHost default in docs. [\#1239](https://github.com/containous/traefik/pull/1239) ([timoreimann](https://github.com/timoreimann))
|
||||
- Update WSS/WS Proto \[Fixes \#670\] [\#1225](https://github.com/containous/traefik/pull/1225) ([dtomcej](https://github.com/dtomcej))
|
||||
- Bump go-rancher version [\#1219](https://github.com/containous/traefik/pull/1219) ([SantoDE](https://github.com/SantoDE))
|
||||
- Chunk taskArns into groups of 100 [\#1209](https://github.com/containous/traefik/pull/1209) ([owen](https://github.com/owen))
|
||||
- Prepare release v1.2.0 rc2 [\#1204](https://github.com/containous/traefik/pull/1204) ([emilevauge](https://github.com/emilevauge))
|
||||
- Revert "Ensure that we don't add balancees with no health check runs … [\#1198](https://github.com/containous/traefik/pull/1198) ([jangie](https://github.com/jangie))
|
||||
- Small fixes and improvments [\#1173](https://github.com/containous/traefik/pull/1173) ([SantoDE](https://github.com/SantoDE))
|
||||
- Fix docker issues with global and dead tasks [\#1167](https://github.com/containous/traefik/pull/1167) ([christopherobin](https://github.com/christopherobin))
|
||||
- Better ECS error checking [\#1143](https://github.com/containous/traefik/pull/1143) ([lpetre](https://github.com/lpetre))
|
||||
- Fix stats race condition [\#1141](https://github.com/containous/traefik/pull/1141) ([emilevauge](https://github.com/emilevauge))
|
||||
- ECS: Docs - info about cred. resolution and required access policies [\#1137](https://github.com/containous/traefik/pull/1137) ([rickard-von-essen](https://github.com/rickard-von-essen))
|
||||
- Healthcheck tests and doc [\#1132](https://github.com/containous/traefik/pull/1132) ([Juliens](https://github.com/Juliens))
|
||||
- Fix travis deploy [\#1128](https://github.com/containous/traefik/pull/1128) ([emilevauge](https://github.com/emilevauge))
|
||||
- Prepare release v1.2.0 rc1 [\#1126](https://github.com/containous/traefik/pull/1126) ([emilevauge](https://github.com/emilevauge))
|
||||
- Fix checkout initial before calling rmpr [\#1124](https://github.com/containous/traefik/pull/1124) ([emilevauge](https://github.com/emilevauge))
|
||||
- Feature rancher integration [\#1120](https://github.com/containous/traefik/pull/1120) ([SantoDE](https://github.com/SantoDE))
|
||||
- Fix glide go units [\#1119](https://github.com/containous/traefik/pull/1119) ([emilevauge](https://github.com/emilevauge))
|
||||
- Carry \#818 — Add systemd watchdog feature [\#1116](https://github.com/containous/traefik/pull/1116) ([vdemeester](https://github.com/vdemeester))
|
||||
- Skip file permission check on Windows [\#1115](https://github.com/containous/traefik/pull/1115) ([StefanScherer](https://github.com/StefanScherer))
|
||||
- Fix Docker API version for Windows [\#1113](https://github.com/containous/traefik/pull/1113) ([StefanScherer](https://github.com/StefanScherer))
|
||||
- Fix git rpr [\#1109](https://github.com/containous/traefik/pull/1109) ([emilevauge](https://github.com/emilevauge))
|
||||
- Fix docker version specifier [\#1108](https://github.com/containous/traefik/pull/1108) ([timoreimann](https://github.com/timoreimann))
|
||||
- Merge v1.1.2 master [\#1105](https://github.com/containous/traefik/pull/1105) ([emilevauge](https://github.com/emilevauge))
|
||||
- add sh before script in deploy... [\#1103](https://github.com/containous/traefik/pull/1103) ([emilevauge](https://github.com/emilevauge))
|
||||
- \[doc\] typo fixes for kubernetes user guide [\#1102](https://github.com/containous/traefik/pull/1102) ([bamarni](https://github.com/bamarni))
|
||||
- add skip\_cleanup in deploy [\#1101](https://github.com/containous/traefik/pull/1101) ([emilevauge](https://github.com/emilevauge))
|
||||
- Fix k8s example UI port. [\#1098](https://github.com/containous/traefik/pull/1098) ([ddunkin](https://github.com/ddunkin))
|
||||
- Fix marathon provider [\#1090](https://github.com/containous/traefik/pull/1090) ([diegooliveira](https://github.com/diegooliveira))
|
||||
- Add an ECS provider [\#1088](https://github.com/containous/traefik/pull/1088) ([lpetre](https://github.com/lpetre))
|
||||
- Update comment to reflect the code [\#1087](https://github.com/containous/traefik/pull/1087) ([np](https://github.com/np))
|
||||
- update NYTimes/gziphandler fixes \#1059 [\#1084](https://github.com/containous/traefik/pull/1084) ([JamesKyburz](https://github.com/JamesKyburz))
|
||||
- Ensure that we don't add balancees with no health check runs if there is a health check defined on it [\#1080](https://github.com/containous/traefik/pull/1080) ([jangie](https://github.com/jangie))
|
||||
- Add FreeBSD & OpenBSD to crossbinary [\#1078](https://github.com/containous/traefik/pull/1078) ([geoffgarside](https://github.com/geoffgarside))
|
||||
- Fix metrics for multiple entry points [\#1071](https://github.com/containous/traefik/pull/1071) ([matevzmihalic](https://github.com/matevzmihalic))
|
||||
- Allow setting load balancer method and sticky using service annotations [\#1068](https://github.com/containous/traefik/pull/1068) ([bakins](https://github.com/bakins))
|
||||
- Fix travis script [\#1067](https://github.com/containous/traefik/pull/1067) ([emilevauge](https://github.com/emilevauge))
|
||||
- Add missing fmt verb specifier in k8s provider. [\#1066](https://github.com/containous/traefik/pull/1066) ([timoreimann](https://github.com/timoreimann))
|
||||
- Add git rpr command [\#1063](https://github.com/containous/traefik/pull/1063) ([emilevauge](https://github.com/emilevauge))
|
||||
- Fix k8s example [\#1062](https://github.com/containous/traefik/pull/1062) ([emilevauge](https://github.com/emilevauge))
|
||||
- Replace underscores to dash in autogenerated urls \(docker provider\) [\#1061](https://github.com/containous/traefik/pull/1061) ([WTFKr0](https://github.com/WTFKr0))
|
||||
- Don't run go test on .glide cache folder [\#1057](https://github.com/containous/traefik/pull/1057) ([vdemeester](https://github.com/vdemeester))
|
||||
- Allow setting circuitbreaker expression via Kubernetes annotation [\#1056](https://github.com/containous/traefik/pull/1056) ([bakins](https://github.com/bakins))
|
||||
- Improving instrumentation. [\#1042](https://github.com/containous/traefik/pull/1042) ([enxebre](https://github.com/enxebre))
|
||||
- Update user guide for upcoming `docker stack deploy` [\#1041](https://github.com/containous/traefik/pull/1041) ([twelvelabs](https://github.com/twelvelabs))
|
||||
- Support sticky sessions under SWARM Mode. \#1024 [\#1033](https://github.com/containous/traefik/pull/1033) ([foleymic](https://github.com/foleymic))
|
||||
- Allow for wildcards in k8s ingress host, fixes \#792 [\#1029](https://github.com/containous/traefik/pull/1029) ([sheerun](https://github.com/sheerun))
|
||||
- Don't fetch ACME certificates for frontends using non-TLS entrypoints \(\#989\) [\#1023](https://github.com/containous/traefik/pull/1023) ([syfonseq](https://github.com/syfonseq))
|
||||
- Return Proper Non-ACME certificate - Fixes Issue 672 [\#1018](https://github.com/containous/traefik/pull/1018) ([dtomcej](https://github.com/dtomcej))
|
||||
- Fix docs build and add missing benchmarks page [\#1017](https://github.com/containous/traefik/pull/1017) ([csabapalfi](https://github.com/csabapalfi))
|
||||
- Set a NopCloser request body with retry middleware [\#1016](https://github.com/containous/traefik/pull/1016) ([bamarni](https://github.com/bamarni))
|
||||
- instruct to flatten dependencies with glide [\#1010](https://github.com/containous/traefik/pull/1010) ([bamarni](https://github.com/bamarni))
|
||||
- check permissions on acme.json during startup [\#1009](https://github.com/containous/traefik/pull/1009) ([bamarni](https://github.com/bamarni))
|
||||
- \[doc\] few tweaks on the basics page [\#1005](https://github.com/containous/traefik/pull/1005) ([bamarni](https://github.com/bamarni))
|
||||
- Import order as goimports does [\#1004](https://github.com/containous/traefik/pull/1004) ([vdemeester](https://github.com/vdemeester))
|
||||
- See the right go report badge [\#991](https://github.com/containous/traefik/pull/991) ([guilhem](https://github.com/guilhem))
|
||||
- Add multiple values for one rule to docs [\#978](https://github.com/containous/traefik/pull/978) ([j0hnsmith](https://github.com/j0hnsmith))
|
||||
- Add ACME/Let’s Encrypt integration tests [\#975](https://github.com/containous/traefik/pull/975) ([trecloux](https://github.com/trecloux))
|
||||
- deploy.sh: upload release source tarball [\#969](https://github.com/containous/traefik/pull/969) ([Mic92](https://github.com/Mic92))
|
||||
- toml zookeeper doc fix [\#948](https://github.com/containous/traefik/pull/948) ([brdude](https://github.com/brdude))
|
||||
- Add Rule AddPrefix [\#931](https://github.com/containous/traefik/pull/931) ([Juliens](https://github.com/Juliens))
|
||||
- Add bug command [\#921](https://github.com/containous/traefik/pull/921) ([emilevauge](https://github.com/emilevauge))
|
||||
- \(WIP\) feat: HealthCheck [\#918](https://github.com/containous/traefik/pull/918) ([Juliens](https://github.com/Juliens))
|
||||
- Add ability to set authenticated user in request header [\#889](https://github.com/containous/traefik/pull/889) ([ViViDboarder](https://github.com/ViViDboarder))
|
||||
- IP-per-task: [\#841](https://github.com/containous/traefik/pull/841) ([diegooliveira](https://github.com/diegooliveira))
|
||||
|
||||
## [v1.2.0-rc2](https://github.com/containous/traefik/tree/v1.2.0-rc2) (2017-03-01)
|
||||
[Full Changelog](https://github.com/containous/traefik/compare/v1.2.0-rc1...v1.2.0-rc2)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Are there plans to support the service type ExternalName in Kubernetes? [\#1142](https://github.com/containous/traefik/issues/1142)
|
||||
- Kubernetes Ingress and sticky support [\#911](https://github.com/containous/traefik/issues/911)
|
||||
- kubernetes client does not support InsecureSkipVerify [\#876](https://github.com/containous/traefik/issues/876)
|
||||
- Support active health checking like HAProxy [\#824](https://github.com/containous/traefik/issues/824)
|
||||
- Allow k8s ingress controller serviceAccountToken and serviceAccountCACert to be changed [\#611](https://github.com/containous/traefik/issues/611)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- \[rancher\] invalid memory address or nil pointer dereference [\#1134](https://github.com/containous/traefik/issues/1134)
|
||||
- Kubernetes default backend should work [\#1073](https://github.com/containous/traefik/issues/1073)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Are release Download links broken? [\#1201](https://github.com/containous/traefik/issues/1201)
|
||||
- Bind to specific ip address [\#1193](https://github.com/containous/traefik/issues/1193)
|
||||
- DNS01 challenge use the wrong zone through route53 [\#1192](https://github.com/containous/traefik/issues/1192)
|
||||
- Reverse proxy https to http backends fails [\#1180](https://github.com/containous/traefik/issues/1180)
|
||||
- Swarm Mode + Letsecrypt + KV Store [\#1176](https://github.com/containous/traefik/issues/1176)
|
||||
- docker deploy -c example.yml e [\#1169](https://github.com/containous/traefik/issues/1169)
|
||||
- Traefik not finding dynamically added services \(Docker Swarm Mode\) [\#1168](https://github.com/containous/traefik/issues/1168)
|
||||
- Traefik with Kubernetes backend - keep getting 401 on all GET requests to kube-apiserver [\#1166](https://github.com/containous/traefik/issues/1166)
|
||||
- Near line 15 \(last key parsed 'backends.backend-monitor-viz.servers'\): Key 'backends.backend-monitor-viz.servers.server-monitor\_viz-1' has already been defined. [\#1154](https://github.com/containous/traefik/issues/1154)
|
||||
- How to reuse SSL certificates automatically fetched from Let´s encrypt? [\#1152](https://github.com/containous/traefik/issues/1152)
|
||||
- Dynamically ban ip when backend repeatedly returns specified status code. \( 403 \) [\#1136](https://github.com/containous/traefik/issues/1136)
|
||||
- Always get 404 accessing my nginx backend service [\#1112](https://github.com/containous/traefik/issues/1112)
|
||||
- Incomplete Docu [\#1091](https://github.com/containous/traefik/issues/1091)
|
||||
- LoadCertificateForDomains: runtime error: invalid memory address [\#1069](https://github.com/containous/traefik/issues/1069)
|
||||
- Traefik creating backends & mappings for ingress annotated with ingress.class: nginx [\#1058](https://github.com/containous/traefik/issues/1058)
|
||||
- ACME file format description [\#1012](https://github.com/containous/traefik/issues/1012)
|
||||
- SwarmMode - Not routing on worker node [\#838](https://github.com/containous/traefik/issues/838)
|
||||
- Migrate k8s to kubernetes/client-go [\#678](https://github.com/containous/traefik/issues/678)
|
||||
- Support for sticky session with kubernetes ingress as backend [\#674](https://github.com/containous/traefik/issues/674)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Revert "Ensure that we don't add balancees with no health check runs … [\#1198](https://github.com/containous/traefik/pull/1198) ([jangie](https://github.com/jangie))
|
||||
- Small fixes and improvments [\#1173](https://github.com/containous/traefik/pull/1173) ([SantoDE](https://github.com/SantoDE))
|
||||
- Fix docker issues with global and dead tasks [\#1167](https://github.com/containous/traefik/pull/1167) ([christopherobin](https://github.com/christopherobin))
|
||||
- Better ECS error checking [\#1143](https://github.com/containous/traefik/pull/1143) ([lpetre](https://github.com/lpetre))
|
||||
- Fix stats race condition [\#1141](https://github.com/containous/traefik/pull/1141) ([emilevauge](https://github.com/emilevauge))
|
||||
- ECS: Docs - info about cred. resolution and required access policies [\#1137](https://github.com/containous/traefik/pull/1137) ([rickard-von-essen](https://github.com/rickard-von-essen))
|
||||
- Healthcheck tests and doc [\#1132](https://github.com/containous/traefik/pull/1132) ([Juliens](https://github.com/Juliens))
|
||||
|
||||
## [v1.2.0-rc1](https://github.com/containous/traefik/tree/v1.2.0-rc1) (2017-02-06)
|
||||
[Full Changelog](https://github.com/containous/traefik/compare/v1.1.2...v1.2.0-rc1)
|
||||
|
||||
**Implemented enhancements:**
|
||||
|
||||
- Add FreeBSD and OpenBSD to release builds [\#923](https://github.com/containous/traefik/issues/923)
|
||||
- Write authenticated user to header key [\#802](https://github.com/containous/traefik/issues/802)
|
||||
- Question: Wildcard Host for Kubernetes Ingress [\#792](https://github.com/containous/traefik/issues/792)
|
||||
- First commit prometheus middleware. [\#1022](https://github.com/containous/traefik/pull/1022) ([enxebre](https://github.com/enxebre))
|
||||
- Use deployment primitives from travis [\#843](https://github.com/containous/traefik/pull/843) ([guilhem](https://github.com/guilhem))
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Increase Docker API version to work with Windows Containers [\#1094](https://github.com/containous/traefik/issues/1094)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- How could I know whether forwarding path is correctly set? [\#1111](https://github.com/containous/traefik/issues/1111)
|
||||
- ACME + Docker-compose labels [\#1099](https://github.com/containous/traefik/issues/1099)
|
||||
- Loadbalance between 2 containers in Docker Swarm Mode [\#1095](https://github.com/containous/traefik/issues/1095)
|
||||
- Add DNS01 letsencrypt challenge support through AWS. [\#1093](https://github.com/containous/traefik/issues/1093)
|
||||
- New Release Cut [\#1092](https://github.com/containous/traefik/issues/1092)
|
||||
- Marathon integration changed default backend server port from task-level to application-level [\#1072](https://github.com/containous/traefik/issues/1072)
|
||||
- websockets not working when compress = true in toml config. [\#1059](https://github.com/containous/traefik/issues/1059)
|
||||
- Proxying 403 http status into the application [\#1044](https://github.com/containous/traefik/issues/1044)
|
||||
- Normalize auto generated frontend-rule \(docker\) [\#1043](https://github.com/containous/traefik/issues/1043)
|
||||
- Traefik with Consul catalog backend + Registrator [\#1039](https://github.com/containous/traefik/issues/1039)
|
||||
- \[Configuration help\] Can't connect to docker containers under a domain path [\#1032](https://github.com/containous/traefik/issues/1032)
|
||||
- Kubernetes and etcd backend : `storeconfig` fails. [\#1031](https://github.com/containous/traefik/issues/1031)
|
||||
- kubernetes: Undefined backend 'X/' for frontend X/" [\#1026](https://github.com/containous/traefik/issues/1026)
|
||||
- TLS handshake error [\#1025](https://github.com/containous/traefik/issues/1025)
|
||||
- Traefik failing on POST request [\#1008](https://github.com/containous/traefik/issues/1008)
|
||||
- how config traffic.toml http 80 without basic auth, traefik WebUI 8080 with basic auth [\#1001](https://github.com/containous/traefik/issues/1001)
|
||||
- Docs 404 [\#995](https://github.com/containous/traefik/issues/995)
|
||||
- Disable acme for non https endpoints [\#989](https://github.com/containous/traefik/issues/989)
|
||||
- Add parameter to configure TLS entrypoints with ca-bundle file [\#984](https://github.com/containous/traefik/issues/984)
|
||||
- docker multiple networks routing [\#970](https://github.com/containous/traefik/issues/970)
|
||||
- don't add Docker containers not on the same network as traefik [\#959](https://github.com/containous/traefik/issues/959)
|
||||
- Multiple frontend routes [\#957](https://github.com/containous/traefik/issues/957)
|
||||
- SNI based routing without TLS offloading [\#933](https://github.com/containous/traefik/issues/933)
|
||||
- NEO4J + traefik proxy Issues [\#907](https://github.com/containous/traefik/issues/907)
|
||||
- ACME OnDemand ignores entrypoint certificate [\#672](https://github.com/containous/traefik/issues/672)
|
||||
- Ability to use self-signed certificates for local development [\#399](https://github.com/containous/traefik/issues/399)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix checkout initial before calling rmpr [\#1124](https://github.com/containous/traefik/pull/1124) ([emilevauge](https://github.com/emilevauge))
|
||||
- Feature rancher integration [\#1120](https://github.com/containous/traefik/pull/1120) ([SantoDE](https://github.com/SantoDE))
|
||||
- Fix glide go units [\#1119](https://github.com/containous/traefik/pull/1119) ([emilevauge](https://github.com/emilevauge))
|
||||
- Carry \#818 — Add systemd watchdog feature [\#1116](https://github.com/containous/traefik/pull/1116) ([vdemeester](https://github.com/vdemeester))
|
||||
- Skip file permission check on Windows [\#1115](https://github.com/containous/traefik/pull/1115) ([StefanScherer](https://github.com/StefanScherer))
|
||||
- Fix Docker API version for Windows [\#1113](https://github.com/containous/traefik/pull/1113) ([StefanScherer](https://github.com/StefanScherer))
|
||||
- Fix git rpr [\#1109](https://github.com/containous/traefik/pull/1109) ([emilevauge](https://github.com/emilevauge))
|
||||
- Fix docker version specifier [\#1108](https://github.com/containous/traefik/pull/1108) ([timoreimann](https://github.com/timoreimann))
|
||||
- Merge v1.1.2 master [\#1105](https://github.com/containous/traefik/pull/1105) ([emilevauge](https://github.com/emilevauge))
|
||||
- add sh before script in deploy... [\#1103](https://github.com/containous/traefik/pull/1103) ([emilevauge](https://github.com/emilevauge))
|
||||
- \[doc\] typo fixes for kubernetes user guide [\#1102](https://github.com/containous/traefik/pull/1102) ([bamarni](https://github.com/bamarni))
|
||||
- add skip\_cleanup in deploy [\#1101](https://github.com/containous/traefik/pull/1101) ([emilevauge](https://github.com/emilevauge))
|
||||
- Fix k8s example UI port. [\#1098](https://github.com/containous/traefik/pull/1098) ([ddunkin](https://github.com/ddunkin))
|
||||
- Fix marathon provider [\#1090](https://github.com/containous/traefik/pull/1090) ([diegooliveira](https://github.com/diegooliveira))
|
||||
- Add an ECS provider [\#1088](https://github.com/containous/traefik/pull/1088) ([lpetre](https://github.com/lpetre))
|
||||
- Update comment to reflect the code [\#1087](https://github.com/containous/traefik/pull/1087) ([np](https://github.com/np))
|
||||
- update NYTimes/gziphandler fixes \#1059 [\#1084](https://github.com/containous/traefik/pull/1084) ([JamesKyburz](https://github.com/JamesKyburz))
|
||||
- Ensure that we don't add balancees with no health check runs if there is a health check defined on it [\#1080](https://github.com/containous/traefik/pull/1080) ([jangie](https://github.com/jangie))
|
||||
- Add FreeBSD & OpenBSD to crossbinary [\#1078](https://github.com/containous/traefik/pull/1078) ([geoffgarside](https://github.com/geoffgarside))
|
||||
- Fix metrics for multiple entry points [\#1071](https://github.com/containous/traefik/pull/1071) ([matevzmihalic](https://github.com/matevzmihalic))
|
||||
- Allow setting load balancer method and sticky using service annotations [\#1068](https://github.com/containous/traefik/pull/1068) ([bakins](https://github.com/bakins))
|
||||
- Fix travis script [\#1067](https://github.com/containous/traefik/pull/1067) ([emilevauge](https://github.com/emilevauge))
|
||||
- Add missing fmt verb specifier in k8s provider. [\#1066](https://github.com/containous/traefik/pull/1066) ([timoreimann](https://github.com/timoreimann))
|
||||
- Add git rpr command [\#1063](https://github.com/containous/traefik/pull/1063) ([emilevauge](https://github.com/emilevauge))
|
||||
- Fix k8s example [\#1062](https://github.com/containous/traefik/pull/1062) ([emilevauge](https://github.com/emilevauge))
|
||||
- Replace underscores to dash in autogenerated urls \(docker provider\) [\#1061](https://github.com/containous/traefik/pull/1061) ([WTFKr0](https://github.com/WTFKr0))
|
||||
- Don't run go test on .glide cache folder [\#1057](https://github.com/containous/traefik/pull/1057) ([vdemeester](https://github.com/vdemeester))
|
||||
- Allow setting circuitbreaker expression via Kubernetes annotation [\#1056](https://github.com/containous/traefik/pull/1056) ([bakins](https://github.com/bakins))
|
||||
- Improving instrumentation. [\#1042](https://github.com/containous/traefik/pull/1042) ([enxebre](https://github.com/enxebre))
|
||||
- Update user guide for upcoming `docker stack deploy` [\#1041](https://github.com/containous/traefik/pull/1041) ([twelvelabs](https://github.com/twelvelabs))
|
||||
- Support sticky sessions under SWARM Mode. \#1024 [\#1033](https://github.com/containous/traefik/pull/1033) ([foleymic](https://github.com/foleymic))
|
||||
- Allow for wildcards in k8s ingress host, fixes \#792 [\#1029](https://github.com/containous/traefik/pull/1029) ([sheerun](https://github.com/sheerun))
|
||||
- Don't fetch ACME certificates for frontends using non-TLS entrypoints \(\#989\) [\#1023](https://github.com/containous/traefik/pull/1023) ([syfonseq](https://github.com/syfonseq))
|
||||
- Return Proper Non-ACME certificate - Fixes Issue 672 [\#1018](https://github.com/containous/traefik/pull/1018) ([dtomcej](https://github.com/dtomcej))
|
||||
- Fix docs build and add missing benchmarks page [\#1017](https://github.com/containous/traefik/pull/1017) ([csabapalfi](https://github.com/csabapalfi))
|
||||
- Set a NopCloser request body with retry middleware [\#1016](https://github.com/containous/traefik/pull/1016) ([bamarni](https://github.com/bamarni))
|
||||
- instruct to flatten dependencies with glide [\#1010](https://github.com/containous/traefik/pull/1010) ([bamarni](https://github.com/bamarni))
|
||||
- check permissions on acme.json during startup [\#1009](https://github.com/containous/traefik/pull/1009) ([bamarni](https://github.com/bamarni))
|
||||
- \[doc\] few tweaks on the basics page [\#1005](https://github.com/containous/traefik/pull/1005) ([bamarni](https://github.com/bamarni))
|
||||
- Import order as goimports does [\#1004](https://github.com/containous/traefik/pull/1004) ([vdemeester](https://github.com/vdemeester))
|
||||
- See the right go report badge [\#991](https://github.com/containous/traefik/pull/991) ([guilhem](https://github.com/guilhem))
|
||||
- Add multiple values for one rule to docs [\#978](https://github.com/containous/traefik/pull/978) ([j0hnsmith](https://github.com/j0hnsmith))
|
||||
- Add ACME/Let’s Encrypt integration tests [\#975](https://github.com/containous/traefik/pull/975) ([trecloux](https://github.com/trecloux))
|
||||
- deploy.sh: upload release source tarball [\#969](https://github.com/containous/traefik/pull/969) ([Mic92](https://github.com/Mic92))
|
||||
- toml zookeeper doc fix [\#948](https://github.com/containous/traefik/pull/948) ([brdude](https://github.com/brdude))
|
||||
- Add Rule AddPrefix [\#931](https://github.com/containous/traefik/pull/931) ([Juliens](https://github.com/Juliens))
|
||||
- Add bug command [\#921](https://github.com/containous/traefik/pull/921) ([emilevauge](https://github.com/emilevauge))
|
||||
- \(WIP\) feat: HealthCheck [\#918](https://github.com/containous/traefik/pull/918) ([Juliens](https://github.com/Juliens))
|
||||
- Add ability to set authenticated user in request header [\#889](https://github.com/containous/traefik/pull/889) ([ViViDboarder](https://github.com/ViViDboarder))
|
||||
- IP-per-task: [\#841](https://github.com/containous/traefik/pull/841) ([diegooliveira](https://github.com/diegooliveira))
|
||||
|
||||
## [v1.1.2](https://github.com/containous/traefik/tree/v1.1.2) (2016-12-15)
|
||||
[Full Changelog](https://github.com/containous/traefik/compare/v1.1.1...v1.1.2)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Problem during HTTPS redirection [\#952](https://github.com/containous/traefik/issues/952)
|
||||
- nil pointer with kubernetes ingress [\#934](https://github.com/containous/traefik/issues/934)
|
||||
- ConsulCatalog and File not working [\#903](https://github.com/containous/traefik/issues/903)
|
||||
- Traefik can not start [\#902](https://github.com/containous/traefik/issues/902)
|
||||
- Cannot connect to Kubernetes server failed to decode watch event [\#532](https://github.com/containous/traefik/issues/532)
|
||||
|
||||
**Closed issues:**
|
||||
|
||||
- Updating certificates with configuration file. [\#968](https://github.com/containous/traefik/issues/968)
|
||||
- Let's encrypt retrieving certificate from wrong IP [\#962](https://github.com/containous/traefik/issues/962)
|
||||
- let's encrypt and dashboard? [\#961](https://github.com/containous/traefik/issues/961)
|
||||
- Working HTTPS example for GKE? [\#960](https://github.com/containous/traefik/issues/960)
|
||||
- GKE design pattern [\#958](https://github.com/containous/traefik/issues/958)
|
||||
- Consul Catalog constraints does not seem to work [\#954](https://github.com/containous/traefik/issues/954)
|
||||
- Issue in building traefik from master [\#949](https://github.com/containous/traefik/issues/949)
|
||||
- Proxy http application to https doesn't seem to work correctly for all services [\#937](https://github.com/containous/traefik/issues/937)
|
||||
- Excessive requests to kubernetes apiserver [\#922](https://github.com/containous/traefik/issues/922)
|
||||
- I am getting a connection error while creating traefik with consul backend "dial tcp 127.0.0.1:8500: getsockopt: connection refused" [\#917](https://github.com/containous/traefik/issues/917)
|
||||
- SwarmMode - 1.13 RC2 - DNS RR - Individual IPs not retrieved [\#913](https://github.com/containous/traefik/issues/913)
|
||||
- Panic in kubernetes ingress \(traefik 1.1.0\) [\#910](https://github.com/containous/traefik/issues/910)
|
||||
- Kubernetes updating deployment image requires Ingress to be remade [\#909](https://github.com/containous/traefik/issues/909)
|
||||
- \[ACME\] Too many currently pending authorizations [\#905](https://github.com/containous/traefik/issues/905)
|
||||
- WEB UI Authentication and Let's Encrypt : error 404 [\#754](https://github.com/containous/traefik/issues/754)
|
||||
- Traefik as ingress controller for SNI based routing in kubernetes [\#745](https://github.com/containous/traefik/issues/745)
|
||||
- Kubernetes Ingress backend: using self-signed certificates [\#486](https://github.com/containous/traefik/issues/486)
|
||||
- Kubernetes Ingress backend: can't find token and ca.crt [\#484](https://github.com/containous/traefik/issues/484)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix duplicate acme certificates [\#972](https://github.com/containous/traefik/pull/972) ([emilevauge](https://github.com/emilevauge))
|
||||
- Fix leadership panic [\#956](https://github.com/containous/traefik/pull/956) ([emilevauge](https://github.com/emilevauge))
|
||||
- Fix redirect regex [\#947](https://github.com/containous/traefik/pull/947) ([emilevauge](https://github.com/emilevauge))
|
||||
- Add operation recover [\#944](https://github.com/containous/traefik/pull/944) ([emilevauge](https://github.com/emilevauge))
|
||||
|
||||
## [v1.1.1](https://github.com/containous/traefik/tree/v1.1.1) (2016-11-29)
|
||||
[Full Changelog](https://github.com/containous/traefik/compare/v1.1.0...v1.1.1)
|
||||
|
||||
|
11
Makefile
11
Makefile
@@ -13,7 +13,7 @@ SRCS = $(shell git ls-files '*.go' | grep -v '^external/')
|
||||
BIND_DIR := "dist"
|
||||
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)
|
||||
GIT_BRANCH := $(subst heads/,,$(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),"containous/traefik")
|
||||
@@ -45,7 +45,7 @@ test-integration: build ## run the integration tests
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate test-integration
|
||||
|
||||
validate: build ## validate gofmt, golint and go vet
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt validate-govet validate-golint
|
||||
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-glide validate-gofmt validate-govet validate-golint validate-misspell
|
||||
|
||||
build: dist
|
||||
docker build $(DOCKER_BUILD_ARGS) -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile .
|
||||
@@ -82,12 +82,5 @@ lint:
|
||||
|
||||
fmt:
|
||||
gofmt -s -l -w $(SRCS)
|
||||
|
||||
deploy:
|
||||
./script/deploy.sh
|
||||
|
||||
deploy-pr:
|
||||
./script/deploy-pr.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)
|
||||
|
@@ -5,15 +5,15 @@
|
||||
|
||||
[](https://travis-ci.org/containous/traefik)
|
||||
[](https://docs.traefik.io)
|
||||
[](http://goreportcard.com/report/containous/traefik)
|
||||
[](http://goreportcard.com/report/containous/traefik)
|
||||
[](https://microbadger.com/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](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Kubernetes](http://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Mesos](https://github.com/apache/mesos), [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.
|
||||
Træfɪk (pronounced like [traffic](https://speak-ipa.bearbin.net/speak.cgi?speak=%CB%88tr%C3%A6f%C9%AAk)) is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
|
||||
It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Kubernetes](http://kubernetes.io), [Marathon](https://mesosphere.github.io/marathon/), [Mesos](https://github.com/apache/mesos), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), [Eureka](https://github.com/Netflix/eureka), Rest API, file...) to manage its configuration automatically and dynamically.
|
||||
|
||||
## Overview
|
||||
|
||||
|
@@ -7,11 +7,14 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/xenolf/lego/acme"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
// Account is used to store lets encrypt registration info
|
||||
@@ -106,6 +109,38 @@ type DomainsCertificates struct {
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (dc *DomainsCertificates) Len() int {
|
||||
return len(dc.Certs)
|
||||
}
|
||||
|
||||
func (dc *DomainsCertificates) Swap(i, j int) {
|
||||
dc.Certs[i], dc.Certs[j] = dc.Certs[j], dc.Certs[i]
|
||||
}
|
||||
|
||||
func (dc *DomainsCertificates) Less(i, j int) bool {
|
||||
if reflect.DeepEqual(dc.Certs[i].Domains, dc.Certs[j].Domains) {
|
||||
return dc.Certs[i].tlsCert.Leaf.NotAfter.After(dc.Certs[j].tlsCert.Leaf.NotAfter)
|
||||
}
|
||||
if dc.Certs[i].Domains.Main == dc.Certs[j].Domains.Main {
|
||||
return strings.Join(dc.Certs[i].Domains.SANs, ",") < strings.Join(dc.Certs[j].Domains.SANs, ",")
|
||||
}
|
||||
return dc.Certs[i].Domains.Main < dc.Certs[j].Domains.Main
|
||||
}
|
||||
|
||||
func (dc *DomainsCertificates) removeDuplicates() {
|
||||
sort.Sort(dc)
|
||||
for i := 0; i < len(dc.Certs); i++ {
|
||||
for i2 := i + 1; i2 < len(dc.Certs); i2++ {
|
||||
if reflect.DeepEqual(dc.Certs[i].Domains, dc.Certs[i2].Domains) {
|
||||
// delete
|
||||
log.Warnf("Remove duplicate cert: %+v, expiration :%s", dc.Certs[i2].Domains, dc.Certs[i2].tlsCert.Leaf.NotAfter.String())
|
||||
dc.Certs = append(dc.Certs[:i2], dc.Certs[i2+1:]...)
|
||||
i2--
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Init inits DomainsCertificates
|
||||
func (dc *DomainsCertificates) Init() error {
|
||||
dc.lock.Lock()
|
||||
@@ -116,7 +151,15 @@ func (dc *DomainsCertificates) Init() error {
|
||||
return err
|
||||
}
|
||||
domainsCertificate.tlsCert = &tlsCert
|
||||
if domainsCertificate.tlsCert.Leaf == nil {
|
||||
leaf, err := x509.ParseCertificate(domainsCertificate.tlsCert.Certificate[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
domainsCertificate.tlsCert.Leaf = leaf
|
||||
}
|
||||
}
|
||||
dc.removeDuplicates()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -192,7 +235,7 @@ func (dc *DomainsCertificate) needRenew() bool {
|
||||
// If there's an error, we assume the cert is broken, and needs update
|
||||
return true
|
||||
}
|
||||
// <= 7 days left, renew certificate
|
||||
// <= 30 days left, renew certificate
|
||||
if crt.NotAfter.Before(time.Now().Add(time.Duration(24 * 30 * time.Hour))) {
|
||||
return true
|
||||
}
|
||||
|
267
acme/acme.go
267
acme/acme.go
@@ -1,9 +1,17 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
fmtlog "log"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
"github.com/cenk/backoff"
|
||||
"github.com/containous/staert"
|
||||
@@ -11,13 +19,14 @@ import (
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/eapache/channels"
|
||||
"github.com/xenolf/lego/acme"
|
||||
"golang.org/x/net/context"
|
||||
"io/ioutil"
|
||||
fmtlog "log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
"github.com/xenolf/lego/providers/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
// OSCPMustStaple enables OSCP stapling as from https://github.com/xenolf/lego/issues/270
|
||||
OSCPMustStaple = false
|
||||
)
|
||||
|
||||
// ACME allows to connect to lets encrypt and retrieve certs
|
||||
@@ -30,11 +39,16 @@ type ACME struct {
|
||||
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
|
||||
CAServer string `description:"CA server to use."`
|
||||
EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
|
||||
DNSProvider string `description:"Use a DNS based challenge provider rather than HTTPS."`
|
||||
DelayDontCheckDNS int `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."`
|
||||
ACMELogging bool `description:"Enable debug logging of ACME actions."`
|
||||
client *acme.Client
|
||||
defaultCertificate *tls.Certificate
|
||||
store cluster.Store
|
||||
challengeProvider *challengeProvider
|
||||
checkOnDemandDomain func(domain string) bool
|
||||
jobs *channels.InfiniteChannel
|
||||
TLSConfig *tls.Config `description:"TLS config in case wildcard certs are used"`
|
||||
}
|
||||
|
||||
//Domains parse []Domain
|
||||
@@ -79,7 +93,11 @@ type Domain struct {
|
||||
}
|
||||
|
||||
func (a *ACME) init() error {
|
||||
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
|
||||
if a.ACMELogging {
|
||||
acme.Logger = fmtlog.New(os.Stderr, "legolog: ", fmtlog.LstdFlags)
|
||||
} else {
|
||||
acme.Logger = fmtlog.New(ioutil.Discard, "", 0)
|
||||
}
|
||||
// no certificates in TLS config, so we add a default one
|
||||
cert, err := generateDefaultCertificate()
|
||||
if err != nil {
|
||||
@@ -91,6 +109,7 @@ func (a *ACME) init() error {
|
||||
log.Warnf("ACME.StorageFile is deprecated, use ACME.Storage instead")
|
||||
a.Storage = a.StorageFile
|
||||
}
|
||||
a.jobs = channels.NewInfiniteChannel()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -106,6 +125,7 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl
|
||||
a.checkOnDemandDomain = checkOnDemandDomain
|
||||
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
|
||||
tlsConfig.GetCertificate = a.getCertificate
|
||||
a.TLSConfig = tlsConfig
|
||||
listener := func(object cluster.Object) error {
|
||||
account := object.(*Account)
|
||||
account.Init()
|
||||
@@ -142,9 +162,7 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := a.renewCertificates(); err != nil {
|
||||
log.Errorf("Error renewing ACME certificate: %s", err.Error())
|
||||
}
|
||||
a.renewCertificates()
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -205,12 +223,10 @@ func (a *ACME) CreateClusterConfig(leadership *cluster.Leadership, tlsConfig *tl
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
safe.Go(func() {
|
||||
a.retrieveCertificates()
|
||||
if err := a.renewCertificates(); err != nil {
|
||||
log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
a.retrieveCertificates()
|
||||
a.renewCertificates()
|
||||
a.runJobs()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -229,7 +245,7 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func
|
||||
a.checkOnDemandDomain = checkOnDemandDomain
|
||||
tlsConfig.Certificates = append(tlsConfig.Certificates, *a.defaultCertificate)
|
||||
tlsConfig.GetCertificate = a.getCertificate
|
||||
|
||||
a.TLSConfig = tlsConfig
|
||||
localStore := NewLocalStore(a.Storage)
|
||||
a.store = localStore
|
||||
a.challengeProvider = &challengeProvider{store: a.store}
|
||||
@@ -295,19 +311,14 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func
|
||||
return err
|
||||
}
|
||||
|
||||
safe.Go(func() {
|
||||
a.retrieveCertificates()
|
||||
if err := a.renewCertificates(); err != nil {
|
||||
log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
|
||||
}
|
||||
})
|
||||
a.retrieveCertificates()
|
||||
a.renewCertificates()
|
||||
a.runJobs()
|
||||
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
safe.Go(func() {
|
||||
for range ticker.C {
|
||||
if err := a.renewCertificates(); err != nil {
|
||||
log.Errorf("Error renewing ACME certificate %+v: %s", account, err.Error())
|
||||
}
|
||||
a.renewCertificates()
|
||||
}
|
||||
|
||||
})
|
||||
@@ -317,6 +328,14 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func
|
||||
func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
domain := types.CanonicalDomain(clientHello.ServerName)
|
||||
account := a.store.Get().(*Account)
|
||||
//use regex to test for wildcard certs that might have been added into TLSConfig
|
||||
for k := range a.TLSConfig.NameToCertificate {
|
||||
selector := "^" + strings.Replace(k, "*.", ".*\\.?", -1) + "$"
|
||||
match, _ := regexp.MatchString(selector, domain)
|
||||
if match {
|
||||
return a.TLSConfig.NameToCertificate[k], nil
|
||||
}
|
||||
}
|
||||
if challengeCert, ok := a.challengeProvider.getCertificate(domain); ok {
|
||||
log.Debugf("ACME got challenge %s", domain)
|
||||
return challengeCert, nil
|
||||
@@ -336,83 +355,101 @@ func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificat
|
||||
}
|
||||
|
||||
func (a *ACME) retrieveCertificates() {
|
||||
log.Infof("Retrieving ACME certificates...")
|
||||
for _, domain := range a.Domains {
|
||||
// check if cert isn't already loaded
|
||||
account := a.store.Get().(*Account)
|
||||
if _, exists := account.DomainsCertificate.exists(domain); !exists {
|
||||
domains := []string{}
|
||||
domains = append(domains, domain.Main)
|
||||
domains = append(domains, domain.SANs...)
|
||||
certificateResource, err := a.getDomainsCertificates(domains)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error())
|
||||
continue
|
||||
}
|
||||
transaction, object, err := a.store.Begin()
|
||||
if err != nil {
|
||||
log.Errorf("Error creating ACME store transaction from domain %s: %s", domain, err.Error())
|
||||
continue
|
||||
}
|
||||
account = object.(*Account)
|
||||
_, err = account.DomainsCertificate.addCertificateForDomains(certificateResource, domain)
|
||||
if err != nil {
|
||||
log.Errorf("Error adding ACME certificate for domain %s: %s", domains, err.Error())
|
||||
continue
|
||||
}
|
||||
a.jobs.In() <- func() {
|
||||
log.Infof("Retrieving ACME certificates...")
|
||||
for _, domain := range a.Domains {
|
||||
// check if cert isn't already loaded
|
||||
account := a.store.Get().(*Account)
|
||||
if _, exists := account.DomainsCertificate.exists(domain); !exists {
|
||||
domains := []string{}
|
||||
domains = append(domains, domain.Main)
|
||||
domains = append(domains, domain.SANs...)
|
||||
certificateResource, err := a.getDomainsCertificates(domains)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting ACME certificate for domain %s: %s", domains, err.Error())
|
||||
continue
|
||||
}
|
||||
transaction, object, err := a.store.Begin()
|
||||
if err != nil {
|
||||
log.Errorf("Error creating ACME store transaction from domain %s: %s", domain, err.Error())
|
||||
continue
|
||||
}
|
||||
account = object.(*Account)
|
||||
_, 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 = transaction.Commit(account); err != nil {
|
||||
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
|
||||
continue
|
||||
if err = transaction.Commit(account); err != nil {
|
||||
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Infof("Retrieved ACME certificates")
|
||||
}
|
||||
log.Infof("Retrieved ACME certificates")
|
||||
}
|
||||
|
||||
func (a *ACME) renewCertificates() error {
|
||||
log.Debugf("Testing certificate renew...")
|
||||
account := a.store.Get().(*Account)
|
||||
for _, certificateResource := range account.DomainsCertificate.Certs {
|
||||
if certificateResource.needRenew() {
|
||||
log.Debugf("Renewing certificate %+v", certificateResource.Domains)
|
||||
renewedCert, err := a.client.RenewCertificate(acme.CertificateResource{
|
||||
Domain: certificateResource.Certificate.Domain,
|
||||
CertURL: certificateResource.Certificate.CertURL,
|
||||
CertStableURL: certificateResource.Certificate.CertStableURL,
|
||||
PrivateKey: certificateResource.Certificate.PrivateKey,
|
||||
Certificate: certificateResource.Certificate.Certificate,
|
||||
}, true)
|
||||
if err != nil {
|
||||
log.Errorf("Error renewing certificate: %v", err)
|
||||
continue
|
||||
}
|
||||
log.Debugf("Renewed certificate %+v", certificateResource.Domains)
|
||||
renewedACMECert := &Certificate{
|
||||
Domain: renewedCert.Domain,
|
||||
CertURL: renewedCert.CertURL,
|
||||
CertStableURL: renewedCert.CertStableURL,
|
||||
PrivateKey: renewedCert.PrivateKey,
|
||||
Certificate: renewedCert.Certificate,
|
||||
}
|
||||
transaction, object, err := a.store.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account = object.(*Account)
|
||||
err = account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains)
|
||||
if err != nil {
|
||||
log.Errorf("Error renewing certificate: %v", err)
|
||||
continue
|
||||
}
|
||||
func (a *ACME) renewCertificates() {
|
||||
a.jobs.In() <- func() {
|
||||
log.Debugf("Testing certificate renew...")
|
||||
account := a.store.Get().(*Account)
|
||||
for _, certificateResource := range account.DomainsCertificate.Certs {
|
||||
if certificateResource.needRenew() {
|
||||
log.Debugf("Renewing certificate %+v", certificateResource.Domains)
|
||||
renewedCert, err := a.client.RenewCertificate(acme.CertificateResource{
|
||||
Domain: certificateResource.Certificate.Domain,
|
||||
CertURL: certificateResource.Certificate.CertURL,
|
||||
CertStableURL: certificateResource.Certificate.CertStableURL,
|
||||
PrivateKey: certificateResource.Certificate.PrivateKey,
|
||||
Certificate: certificateResource.Certificate.Certificate,
|
||||
}, true, OSCPMustStaple)
|
||||
if err != nil {
|
||||
log.Errorf("Error renewing certificate: %v", err)
|
||||
continue
|
||||
}
|
||||
log.Debugf("Renewed certificate %+v", certificateResource.Domains)
|
||||
renewedACMECert := &Certificate{
|
||||
Domain: renewedCert.Domain,
|
||||
CertURL: renewedCert.CertURL,
|
||||
CertStableURL: renewedCert.CertStableURL,
|
||||
PrivateKey: renewedCert.PrivateKey,
|
||||
Certificate: renewedCert.Certificate,
|
||||
}
|
||||
transaction, object, err := a.store.Begin()
|
||||
if err != nil {
|
||||
log.Errorf("Error renewing certificate: %v", err)
|
||||
continue
|
||||
}
|
||||
account = object.(*Account)
|
||||
err = account.DomainsCertificate.renewCertificates(renewedACMECert, certificateResource.Domains)
|
||||
if err != nil {
|
||||
log.Errorf("Error renewing certificate: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = transaction.Commit(account); err != nil {
|
||||
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
|
||||
continue
|
||||
if err = transaction.Commit(account); err != nil {
|
||||
log.Errorf("Error Saving ACME account %+v: %s", account, err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dnsOverrideDelay(delay int) error {
|
||||
var err error
|
||||
if delay > 0 {
|
||||
log.Debugf("Delaying %d seconds rather than validating DNS propagation", delay)
|
||||
acme.PreCheckDNS = func(_, _ string) (bool, error) {
|
||||
time.Sleep(time.Duration(delay) * time.Second)
|
||||
return true, nil
|
||||
}
|
||||
} else if delay < 0 {
|
||||
err = fmt.Errorf("Invalid negative DelayDontCheckDNS: %d", delay)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) {
|
||||
@@ -425,8 +462,28 @@ func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
|
||||
err = client.SetChallengeProvider(acme.TLSSNI01, a.challengeProvider)
|
||||
|
||||
if len(a.DNSProvider) > 0 {
|
||||
log.Debugf("Using DNS Challenge provider: %s", a.DNSProvider)
|
||||
|
||||
err = dnsOverrideDelay(a.DelayDontCheckDNS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var provider acme.ChallengeProvider
|
||||
provider, err = dns.NewDNSChallengeProviderByName(a.DNSProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.TLSSNI01})
|
||||
err = client.SetChallengeProvider(acme.DNS01, provider)
|
||||
} else {
|
||||
client.ExcludeChallenges([]acme.Challenge{acme.HTTP01, acme.DNS01})
|
||||
err = client.SetChallengeProvider(acme.TLSSNI01, a.challengeProvider)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -462,8 +519,9 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C
|
||||
|
||||
// LoadCertificateForDomains loads certificates from ACME for given domains
|
||||
func (a *ACME) LoadCertificateForDomains(domains []string) {
|
||||
domains = fun.Map(types.CanonicalDomain, domains).([]string)
|
||||
safe.Go(func() {
|
||||
a.jobs.In() <- func() {
|
||||
log.Debugf("LoadCertificateForDomains %s...", domains)
|
||||
domains = fun.Map(types.CanonicalDomain, domains).([]string)
|
||||
operation := func() error {
|
||||
if a.client == nil {
|
||||
return fmt.Errorf("ACME client still not built")
|
||||
@@ -475,7 +533,7 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
|
||||
}
|
||||
ebo := backoff.NewExponentialBackOff()
|
||||
ebo.MaxElapsedTime = 30 * time.Second
|
||||
err := backoff.RetryNotify(operation, ebo, notify)
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting ACME client: %v", err)
|
||||
return
|
||||
@@ -517,14 +575,14 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
|
||||
log.Errorf("Error Saving ACME account %+v: %v", account, err)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) {
|
||||
domains = fun.Map(types.CanonicalDomain, domains).([]string)
|
||||
log.Debugf("Loading ACME certificates %s...", domains)
|
||||
bundle := true
|
||||
certificate, failures := a.client.ObtainCertificate(domains, bundle, nil)
|
||||
certificate, failures := a.client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple)
|
||||
if len(failures) > 0 {
|
||||
log.Error(failures)
|
||||
return nil, fmt.Errorf("Cannot obtain certificates %s+v", failures)
|
||||
@@ -538,3 +596,12 @@ func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) {
|
||||
Certificate: certificate.Certificate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *ACME) runJobs() {
|
||||
safe.Go(func() {
|
||||
for job := range a.jobs.Out() {
|
||||
function := job.(func())
|
||||
function()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -1,9 +1,15 @@
|
||||
package acme
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
func TestDomainsSet(t *testing.T) {
|
||||
@@ -62,6 +68,8 @@ func TestDomainsSetAppend(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCertificatesRenew(t *testing.T) {
|
||||
foo1Cert, foo1Key, _ := generateKeyPair("foo1.com", time.Now())
|
||||
foo2Cert, foo2Key, _ := generateKeyPair("foo2.com", time.Now())
|
||||
domainsCertificates := DomainsCertificates{
|
||||
lock: sync.RWMutex{},
|
||||
Certs: []*DomainsCertificate{
|
||||
@@ -73,55 +81,8 @@ func TestCertificatesRenew(t *testing.T) {
|
||||
Domain: "foo1.com",
|
||||
CertURL: "url",
|
||||
CertStableURL: "url",
|
||||
PrivateKey: []byte(`
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA6OqHGdwGy20+3Jcz9IgfN4IR322X2Hhwk6n8Hss/Ws7FeTZo
|
||||
PvXW8uHeI1bmQJsy9C6xo3odzO64o7prgMZl5eDw5fk1mmUij3J3nM3gwtc/Cc+8
|
||||
ADXGldauASdHBFTRvWQge0Pv/Q5U0fyL2VCHoR9mGv4CQ7nRNKPus0vYJMbXoTbO
|
||||
8z4sIbNz3Ov9o/HGMRb8D0rNPTMdC62tHSbiO1UoxLXr9dcBOGt786AsiRTJ8bq9
|
||||
GCVQgzd0Wftb8z6ddW2YuWrmExlkHdfC4oG0D5SU1QB4ldPyl7fhVWlfHwC1NX+c
|
||||
RnDSEeYkAcdvvIekdM/yH+z62XhwToM0E9TCzwIDAQABAoIBACq3EC3S50AZeeTU
|
||||
qgeXizoP1Z1HKQjfFa5PB1jSZ30M3LRdIQMi7NfASo/qmPGSROb5RUS42YxC34PP
|
||||
ZXXJbNiaxzM13/m/wHXURVFxhF3XQc1X1p+nPRMvutulS2Xk9E4qdbaFgBbFsRKN
|
||||
oUwqc6U97+jVWq72/gIManNhXnNn1n1SRLBEkn+WStMPn6ZvWRlpRMjhy0c1mpwg
|
||||
u6em92HvMvfKPQ60naUhdKp+q0rsLp2YKWjiytos9ENSYI5gAGLIDhKeqiD8f92E
|
||||
4FGPmNRipwxCE2SSvZFlM26tRloWVcBPktRN79hUejE8iopiqVS0+4h/phZ2wG0D
|
||||
18cqVpECgYEA+qmagnhm0LLvwVkUN0B2nRARQEFinZDM4Hgiv823bQvc9I8dVTqJ
|
||||
aIQm5y4Y5UA3xmyDsRoO7GUdd0oVeh9GwTONzMRCOny/mOuOC51wXPhKHhI0O22u
|
||||
sfbOHszl+bxl6ZQMUJa2/I8YIWBLU5P+fTgrfNwBEgZ3YPwUV5tyHNcCgYEA7eAv
|
||||
pjQkbJNRq/fv/67sojN7N9QoH84egN5cZFh5d8PJomnsvy5JDV4WaG1G6mJpqjdD
|
||||
YRVdFw5oZ4L8yCVdCeK9op896Uy51jqvfSe3+uKmNqE0qDHgaLubQNI8yYc5sacW
|
||||
fYJBmDR6rNIeE7Q2240w3CdKfREuXdDnhyTTEskCgYBFeAnFTP8Zqe2+hSSQJ4J4
|
||||
BwLw7u4Yww+0yja/N5E1XItRD/TOMRnx6GYrvd/ScVjD2kEpLRKju2ZOMC8BmHdw
|
||||
hgwvitjcAsTK6cWFPI3uhjVsXhkxuzUmR0Naz+iQrQEFmi1LjGmMV1AVt+1IbYSj
|
||||
SZTr1sFJMJeXPmWY3hDjIwKBgQC4H9fCJoorIL0PB5NVreishHzT8fw84ibqSTPq
|
||||
2DDtazcf6C3AresN1c4ydqN1uUdg4fXdp9OujRBzTwirQ4CIrmFrBye89g7CrBo6
|
||||
Hgxivh06G/3OUw0JBG5f9lvnAiy+Pj9CVxi+36A1NU7ioZP0zY0MW71koW/qXlFY
|
||||
YkCfQQKBgBqwND/c3mPg7iY4RMQ9XjrKfV9o6FMzA51lAinjujHlNgsBmqiR951P
|
||||
NA3kWZQ73D3IxeLEMaGHpvS7andPN3Z2qPhe+FbJKcF6ZZNTrFQkh/Fpz3wmYPo1
|
||||
GIL4+09kNgMRWapaROqI+/3+qJQ+GVJZIPfYC0poJOO6vYqifWe8
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`),
|
||||
Certificate: []byte(`
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC+TCCAeGgAwIBAgIJAK78ukR/Qu4rMA0GCSqGSIb3DQEBBQUAMBMxETAPBgNV
|
||||
BAMMCGZvbzEuY29tMB4XDTE2MDYxOTIyMDMyM1oXDTI2MDYxNzIyMDMyM1owEzER
|
||||
MA8GA1UEAwwIZm9vMS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
|
||||
AQDo6ocZ3AbLbT7clzP0iB83ghHfbZfYeHCTqfweyz9azsV5Nmg+9dby4d4jVuZA
|
||||
mzL0LrGjeh3M7rijumuAxmXl4PDl+TWaZSKPcneczeDC1z8Jz7wANcaV1q4BJ0cE
|
||||
VNG9ZCB7Q+/9DlTR/IvZUIehH2Ya/gJDudE0o+6zS9gkxtehNs7zPiwhs3Pc6/2j
|
||||
8cYxFvwPSs09Mx0Lra0dJuI7VSjEtev11wE4a3vzoCyJFMnxur0YJVCDN3RZ+1vz
|
||||
Pp11bZi5auYTGWQd18LigbQPlJTVAHiV0/KXt+FVaV8fALU1f5xGcNIR5iQBx2+8
|
||||
h6R0z/If7PrZeHBOgzQT1MLPAgMBAAGjUDBOMB0GA1UdDgQWBBRFLH1wF6BT51uq
|
||||
yWNqBnCrPFIglzAfBgNVHSMEGDAWgBRFLH1wF6BT51uqyWNqBnCrPFIglzAMBgNV
|
||||
HRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAr7aH3Db6TeAZkg4Zd7SoF2q11
|
||||
erzv552PgQUyezMZcRBo2q1ekmUYyy2600CBiYg51G+8oUqjJKiKnBuaqbMX7pFa
|
||||
FsL7uToZCGA57cBaVejeB+p24P5bxoJGKCMeZcEBe5N93Tqu5WBxNEX7lQUo6TSs
|
||||
gSN2Olf3/grNKt5V4BduSIQZ+YHlPUWLTaz5B1MXKSUqjmabARP9lhjO14u9USvi
|
||||
dMBDFskJySQ6SUfz3fyoXELoDOVbRZETuSodpw+aFCbEtbcQCLT3A0FG+BEPayZH
|
||||
tt19zKUlr6e+YFpyjQPGZ7ZkY7iMgHEkhKrXx2DiZ1+cif3X1xfXWQr0S5+E
|
||||
-----END CERTIFICATE-----
|
||||
`),
|
||||
PrivateKey: foo1Key,
|
||||
Certificate: foo1Cert,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -132,113 +93,19 @@ tt19zKUlr6e+YFpyjQPGZ7ZkY7iMgHEkhKrXx2DiZ1+cif3X1xfXWQr0S5+E
|
||||
Domain: "foo2.com",
|
||||
CertURL: "url",
|
||||
CertStableURL: "url",
|
||||
PrivateKey: []byte(`
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEA7rIVuSrZ3FfYXhR3qaWwfVcgiqKS//yXFzNqkJS6mz9nRCNT
|
||||
lPawvrCFIRKdR7UO7xD7A5VTcbrGOAaTvrEaH7mB/4FGL+gN4AiTbVFpKXngAYEW
|
||||
A3//zeBZ7XUSWaQ+CNC+l796JeoDvQD++KwCke4rVD1pGN1hpVEeGhwzyKOYPKLo
|
||||
4+AGVe1LFWw4U/v8Iil1/gBBehZBILuhASpXy4W132LJPl76/EbGqh0nVz2UlFqU
|
||||
HRxO+2U2ba4YIpI+0/VOQ9Cq/TzHSUdTTLfBHE/Qb+aDBfptMWTRvAngLqUglOcZ
|
||||
Fi6SAljxEkJO6z6btmoVUWsoKBpbIHDC5++dZwIDAQABAoIBAAD8rYhRfAskNdnV
|
||||
vdTuwXcTOCg6md8DHWDULpmgc9EWhwfKGZthFcQEGNjVKd9VCVXFvTP7lxe+TPmI
|
||||
VW4Rb2k4LChxUWf7TqthfbKTBptMTLfU39Ft4xHn3pdTx5qlSjhhHJimCwxDFnbe
|
||||
nS9MDsqpsHYtttSKfc/gMP6spS4sNPZ/r9zseT3eWkBEhn+FQABxJiuPcQ7q7S+Q
|
||||
uOghmr7f3FeYvizQOhBtULsLrK/hsmQIIB4amS1QlpNWKbIoiUPNPjCA5PVQyAER
|
||||
waYjuc7imBbeD98L/z8bRTlEskSKjtPSEXGVHa9OYdBU+02Ci6TjKztUp6Ho7JE9
|
||||
tcHj+eECgYEA+9Ntv6RqIdpT/4/52JYiR+pOem3U8tweCOmUqm/p/AWyfAJTykqt
|
||||
cJ8RcK1MfM+uoa5Sjm8hIcA2XPVEqH2J50PC4w04Q3xtfsz3xs7KJWXQCoha8D0D
|
||||
ZIFNroEPnld0qOuJzpIIteXTrCLhSu17ZhN+Wk+5gJ7Ewu/QMM5OPjECgYEA8qbw
|
||||
zfwSjE6jkrqO70jzqSxgi2yjo0vMqv+BNBuhxhDTBXnKQI1KsHoiS0FkSLSJ9+DS
|
||||
CT3WEescD2Lumdm2s9HXvaMmnDSKBY58NqCGsNzZifSgmj1H/yS9FX8RXfSjXcxq
|
||||
RDvTbD52/HeaCiOxHZx8JjmJEb+ZKJC4MDvjtxcCgYBM516GvgEjYXdxfliAiijh
|
||||
6W4Z+Vyk5g/ODPc3rYG5U0wUjuljx7Z7xDghPusy2oGsIn5XvRxTIE35yXU0N1Jb
|
||||
69eiWzEpeuA9bv7kGdal4RfNf6K15wwYL1y3w/YvFuorg/LLwNEkK5Ge6e//X9Ll
|
||||
c2KM1fgCjXntRitAHGDMoQKBgDnkgodioLpA+N3FDN0iNqAiKlaZcOFA8G/LzfO0
|
||||
tAAhe3dO+2YzT6KTQSNbUqXWDSTKytHRowVbZrJ1FCA4xVJZunNQPaH/Fv8EY7ZU
|
||||
zk3cIzq61qZ2AHtrNIGwc2BLQb7bSm9FJsgojxLlJidNJLC/6Q7lo0JMyCnZfVhk
|
||||
sYu5AoGAZt/MfyFTKm674UddSNgGEt86PyVYbLMnRoAXOaNB38AE12kaYHPil1tL
|
||||
FnL8OQLpbX5Qo2JGgeZRlpMJ4Jxw2zzvUKr/n+6khaLxHmtX48hMu2QM7ZvnkZCs
|
||||
Kkgz6v+Wcqm94ugtl3HSm+u9xZzVQxN6gu/jZQv3VpQiAZHjPYc=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`),
|
||||
Certificate: []byte(`
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC+TCCAeGgAwIBAgIJAK25/Z9Jz6IBMA0GCSqGSIb3DQEBBQUAMBMxETAPBgNV
|
||||
BAMMCGZvbzIuY29tMB4XDTE2MDYyMDA5MzUyNloXDTI2MDYxODA5MzUyNlowEzER
|
||||
MA8GA1UEAwwIZm9vMi5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
|
||||
AQDushW5KtncV9heFHeppbB9VyCKopL//JcXM2qQlLqbP2dEI1OU9rC+sIUhEp1H
|
||||
tQ7vEPsDlVNxusY4BpO+sRofuYH/gUYv6A3gCJNtUWkpeeABgRYDf//N4FntdRJZ
|
||||
pD4I0L6Xv3ol6gO9AP74rAKR7itUPWkY3WGlUR4aHDPIo5g8oujj4AZV7UsVbDhT
|
||||
+/wiKXX+AEF6FkEgu6EBKlfLhbXfYsk+Xvr8RsaqHSdXPZSUWpQdHE77ZTZtrhgi
|
||||
kj7T9U5D0Kr9PMdJR1NMt8EcT9Bv5oMF+m0xZNG8CeAupSCU5xkWLpICWPESQk7r
|
||||
Ppu2ahVRaygoGlsgcMLn751nAgMBAAGjUDBOMB0GA1UdDgQWBBQ6FZWqB9qI4NN+
|
||||
2jFY6xH8uoUTnTAfBgNVHSMEGDAWgBQ6FZWqB9qI4NN+2jFY6xH8uoUTnTAMBgNV
|
||||
HRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQCRhuf2dQhIEOmSOGgtRELF2wB6
|
||||
NWXt0lCty9x4u+zCvITXV8Z0C34VQGencO3H2bgyC3ZxNpPuwZfEc2Pxe8W6bDc/
|
||||
OyLckk9WLo00Tnr2t7rDOeTjEGuhXFZkhIbJbKdAH8cEXrxKR8UXWtZgTv/b8Hv/
|
||||
g6tbeH6TzBsdMoFtUCsyWxygYwnLU+quuYvE2s9FiCegf2mdYTCh/R5J5n/51gfB
|
||||
uC+NakKMfaCvNg3mOAFSYC/0r0YcKM/5ldKGTKTCVJAMhnmBnyRc/70rKkVRFy2g
|
||||
iIjUFs+9aAgfCiL0WlyyXYAtIev2gw4FHUVlcT/xKks+x8Kgj6e5LTIrRRwW
|
||||
-----END CERTIFICATE-----
|
||||
`),
|
||||
PrivateKey: foo2Key,
|
||||
Certificate: foo2Cert,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
foo1Cert, foo1Key, _ = generateKeyPair("foo1.com", time.Now())
|
||||
newCertificate := &Certificate{
|
||||
Domain: "foo1.com",
|
||||
CertURL: "url",
|
||||
CertStableURL: "url",
|
||||
PrivateKey: []byte(`
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA1OdSuXK2zeSLf0UqgrI4pjkpaqhra++pnda4Li4jXo151svi
|
||||
Sn7DSynJOoq1jbfRJAoyDhxsBC4S4RuD54U5elJ4wLPZXmHRsvb+NwiHs9VmDqwu
|
||||
It21btuqeNMebkab5cnDnC6KKufMhXRcRAlluYXyCkQe/+N+LlUQd6Js34TixMpk
|
||||
eQOX4/OVrokSyVRnIq4u+o0Ufe7z5+41WVH63tcy7Hwi7244aLUzZCs+QQa2Dw6f
|
||||
qEwjbonr974fM68UxDjTZEQy9u24yDzajhDBp1OTAAklh7U+li3g9dSyNVBFXqEu
|
||||
nW2fyBvLqeJOSTihqfcrACB/YYhYOX94vMXELQIDAQABAoIBAFYK3t3fxI1VTiMz
|
||||
WsjTKh3TgC+AvVkz1ILbojfXoae22YS7hUrCDD82NgMYx+LsZPOBw1T8m5Lc4/hh
|
||||
3F8W8nHDHtYSWUjRk6QWOgsXwXAmUEahw0uH+qlA0ZZfDC9ZDexCLHHURTat03Qj
|
||||
4J4GhjwCLB2GBlk4IWisLCmNVR7HokrpfIw4oM1aB5E21Tl7zh/x7ikRijEkUsKw
|
||||
7YhaMeLJqBnMnAdV63hhF7FaDRjl8P2s/3octz/6pqDIABrDrUW3KAkNYCZIWdhF
|
||||
Kk0wRMbZ/WrYT9GIGoJe7coQC7ezTrlrEkAFEIPGHCLkgXB/0TyuSy0yY59e4zmi
|
||||
VvHoWUECgYEA/rOL2KJ/p+TZW7+YbsUzs0+F+M+G6UCr0nWfYN9MKmNtmns3eLDG
|
||||
+pIpBMc5mjqeJR/sCCdkD8OqHC202Y8e4sr0pKSBeBofh2BmXtpyu3QQ50Pa63RS
|
||||
SK6mYUrFqPmFFDbNGpFI4sIeI+Vf6hm96FQPnyPtUTGqk39m0RbWM/UCgYEA1f04
|
||||
Nf3wbqwqIHZjYpPmymfjleyMn3hGUjpi7pmI6inXGMk3nkeG1cbOhnfPxL5BWD12
|
||||
3RqHI2B4Z4r0BMyjctDNb1TxhMIpm5+PKm5KeeKfoYA85IS0mEeq6VdMm3mL1x/O
|
||||
3LYvcUvAEVf6pWX/+ZFLMudqhF3jbTrdNOC6ZFkCgYBKpEeJdyW+CD0CvEVpwPUD
|
||||
yXxTjE3XMZKpHLtWYlop2fWW3iFFh1jouci3k8L3xdHuw0oioZibXhYOJ/7l+yFs
|
||||
CVpknakrj0xKGiAmEBKriLojbClN80rh7fzoakc+29D6OY0mCgm4GndGwcO4EU8s
|
||||
NOZXFupHbyy0CRQSloSzuQKBgQC1Z/MtIlefGuijmHlsakGuuR+gS2ZzEj1bHBAe
|
||||
gZ4mFM46PuqdjblqpR0TtaI3AarXqVOI4SJLBU9NR+jR4MF3Zjeh9/q/NvKa8Usn
|
||||
B1Svu0TkXphAiZenuKnVIqLY8tNvzZFKXlAd1b+/dDwR10SHR3rebnxINmfEg7Bf
|
||||
UVvyEQKBgAEjI5O6LSkLNpbVn1l2IO8u8D2RkFqs/Sbx78uFta3f9Gddzb4wMnt3
|
||||
jVzymghCLp4Qf1ump/zC5bcQ8L97qmnjJ+H8X9HwmkqetuI362JNnz+12YKVDIWi
|
||||
wI7SJ8BwDqYMrLw6/nE+degn39KedGDH8gz5cZcdlKTZLjbuBOfU
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`),
|
||||
Certificate: []byte(`
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC+TCCAeGgAwIBAgIJAPQiOiQcwYaRMA0GCSqGSIb3DQEBBQUAMBMxETAPBgNV
|
||||
BAMMCGZvbzEuY29tMB4XDTE2MDYxOTIyMTE1NFoXDTI2MDYxNzIyMTE1NFowEzER
|
||||
MA8GA1UEAwwIZm9vMS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
|
||||
AQDU51K5crbN5It/RSqCsjimOSlqqGtr76md1rguLiNejXnWy+JKfsNLKck6irWN
|
||||
t9EkCjIOHGwELhLhG4PnhTl6UnjAs9leYdGy9v43CIez1WYOrC4i3bVu26p40x5u
|
||||
RpvlycOcLooq58yFdFxECWW5hfIKRB7/434uVRB3omzfhOLEymR5A5fj85WuiRLJ
|
||||
VGciri76jRR97vPn7jVZUfre1zLsfCLvbjhotTNkKz5BBrYPDp+oTCNuiev3vh8z
|
||||
rxTEONNkRDL27bjIPNqOEMGnU5MACSWHtT6WLeD11LI1UEVeoS6dbZ/IG8up4k5J
|
||||
OKGp9ysAIH9hiFg5f3i8xcQtAgMBAAGjUDBOMB0GA1UdDgQWBBQPfkS5ehpstmSb
|
||||
8CGJE7GxSCxl2DAfBgNVHSMEGDAWgBQPfkS5ehpstmSb8CGJE7GxSCxl2DAMBgNV
|
||||
HRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQA99A+itS9ImdGRGgHZ5fSusiEq
|
||||
wkK5XxGyagL1S0f3VM8e78VabSvC0o/xdD7DHVg6Az8FWxkkksH6Yd7IKfZZUzvs
|
||||
kXQhlOwWpxgmguSmAs4uZTymIoMFRVj3nG664BcXkKu4Yd9UXKNOWP59zgvrCJMM
|
||||
oIsmYiq5u0MFpM31BwfmmW3erqIcfBI9OJrmr1XDzlykPZNWtUSSfVuNQ8d4bim9
|
||||
XH8RfVLeFbqDydSTCHIFvYthH/ESbpRCiGJHoJ8QLfOkhD1k2fI0oJZn5RVtG2W8
|
||||
bZME3gHPYCk1QFZUptriMCJ5fMjCgxeOTR+FAkstb/lTRuCc4UyILJguIMar
|
||||
-----END CERTIFICATE-----
|
||||
`),
|
||||
PrivateKey: foo1Key,
|
||||
Certificate: foo1Cert,
|
||||
}
|
||||
|
||||
err := domainsCertificates.renewCertificates(
|
||||
@@ -256,3 +123,157 @@ bZME3gHPYCk1QFZUptriMCJ5fMjCgxeOTR+FAkstb/lTRuCc4UyILJguIMar
|
||||
t.Errorf("Expected new certificate %+v \nGot %+v", newCertificate, domainsCertificates.Certs[0].Certificate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDuplicates(t *testing.T) {
|
||||
now := time.Now()
|
||||
fooCert, fooKey, _ := generateKeyPair("foo.com", now)
|
||||
foo24Cert, foo24Key, _ := generateKeyPair("foo.com", now.Add(24*time.Hour))
|
||||
foo48Cert, foo48Key, _ := generateKeyPair("foo.com", now.Add(48*time.Hour))
|
||||
barCert, barKey, _ := generateKeyPair("bar.com", now)
|
||||
domainsCertificates := DomainsCertificates{
|
||||
lock: sync.RWMutex{},
|
||||
Certs: []*DomainsCertificate{
|
||||
{
|
||||
Domains: Domain{
|
||||
Main: "foo.com",
|
||||
SANs: []string{}},
|
||||
Certificate: &Certificate{
|
||||
Domain: "foo.com",
|
||||
CertURL: "url",
|
||||
CertStableURL: "url",
|
||||
PrivateKey: foo24Key,
|
||||
Certificate: foo24Cert,
|
||||
},
|
||||
},
|
||||
{
|
||||
Domains: Domain{
|
||||
Main: "foo.com",
|
||||
SANs: []string{}},
|
||||
Certificate: &Certificate{
|
||||
Domain: "foo.com",
|
||||
CertURL: "url",
|
||||
CertStableURL: "url",
|
||||
PrivateKey: foo48Key,
|
||||
Certificate: foo48Cert,
|
||||
},
|
||||
},
|
||||
{
|
||||
Domains: Domain{
|
||||
Main: "foo.com",
|
||||
SANs: []string{}},
|
||||
Certificate: &Certificate{
|
||||
Domain: "foo.com",
|
||||
CertURL: "url",
|
||||
CertStableURL: "url",
|
||||
PrivateKey: fooKey,
|
||||
Certificate: fooCert,
|
||||
},
|
||||
},
|
||||
{
|
||||
Domains: Domain{
|
||||
Main: "bar.com",
|
||||
SANs: []string{}},
|
||||
Certificate: &Certificate{
|
||||
Domain: "bar.com",
|
||||
CertURL: "url",
|
||||
CertStableURL: "url",
|
||||
PrivateKey: barKey,
|
||||
Certificate: barCert,
|
||||
},
|
||||
},
|
||||
{
|
||||
Domains: Domain{
|
||||
Main: "foo.com",
|
||||
SANs: []string{}},
|
||||
Certificate: &Certificate{
|
||||
Domain: "foo.com",
|
||||
CertURL: "url",
|
||||
CertStableURL: "url",
|
||||
PrivateKey: foo48Key,
|
||||
Certificate: foo48Cert,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
domainsCertificates.Init()
|
||||
|
||||
if len(domainsCertificates.Certs) != 2 {
|
||||
t.Errorf("Expected domainsCertificates length %d %+v\nGot %+v", 2, domainsCertificates.Certs, len(domainsCertificates.Certs))
|
||||
}
|
||||
|
||||
for _, cert := range domainsCertificates.Certs {
|
||||
switch cert.Domains.Main {
|
||||
case "bar.com":
|
||||
continue
|
||||
case "foo.com":
|
||||
if !cert.tlsCert.Leaf.NotAfter.Equal(now.Add(48 * time.Hour).Truncate(1 * time.Second)) {
|
||||
t.Errorf("Bad expiration %s date for domain %+v, now %s", cert.tlsCert.Leaf.NotAfter.String(), cert, now.Add(48*time.Hour).Truncate(1*time.Second).String())
|
||||
}
|
||||
default:
|
||||
t.Errorf("Unknown domain %+v", cert)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoPreCheckOverride(t *testing.T) {
|
||||
acme.PreCheckDNS = nil // Irreversable - but not expecting real calls into this during testing process
|
||||
err := dnsOverrideDelay(0)
|
||||
if err != nil {
|
||||
t.Errorf("Error in dnsOverrideDelay :%v", err)
|
||||
}
|
||||
if acme.PreCheckDNS != nil {
|
||||
t.Errorf("Unexpected change to acme.PreCheckDNS when leaving DNS verification as is.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSillyPreCheckOverride(t *testing.T) {
|
||||
err := dnsOverrideDelay(-5)
|
||||
if err == nil {
|
||||
t.Errorf("Missing expected error in dnsOverrideDelay!")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreCheckOverride(t *testing.T) {
|
||||
acme.PreCheckDNS = nil // Irreversable - but not expecting real calls into this during testing process
|
||||
err := dnsOverrideDelay(5)
|
||||
if err != nil {
|
||||
t.Errorf("Error in dnsOverrideDelay :%v", err)
|
||||
}
|
||||
if acme.PreCheckDNS == nil {
|
||||
t.Errorf("No change to acme.PreCheckDNS when meant to be adding enforcing override function.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAcmeClientCreation(t *testing.T) {
|
||||
acme.PreCheckDNS = nil // Irreversable - but not expecting real calls into this during testing process
|
||||
// Lengthy setup to avoid external web requests - oh for easier golang testing!
|
||||
account := &Account{Email: "f@f"}
|
||||
account.PrivateKey, _ = base64.StdEncoding.DecodeString(`
|
||||
MIIBPAIBAAJBAMp2Ni92FfEur+CAvFkgC12LT4l9D53ApbBpDaXaJkzzks+KsLw9zyAxvlrfAyTCQ
|
||||
7tDnEnIltAXyQ0uOFUUdcMCAwEAAQJAK1FbipATZcT9cGVa5x7KD7usytftLW14heQUPXYNV80r/3
|
||||
lmnpvjL06dffRpwkYeN8DATQF/QOcy3NNNGDw/4QIhAPAKmiZFxA/qmRXsuU8Zhlzf16WrNZ68K64
|
||||
asn/h3qZrAiEA1+wFR3WXCPIolOvd7AHjfgcTKQNkoMPywU4FYUNQ1AkCIQDv8yk0qPjckD6HVCPJ
|
||||
llJh9MC0svjevGtNlxJoE3lmEQIhAKXy1wfZ32/XtcrnENPvi6lzxI0T94X7s5pP3aCoPPoJAiEAl
|
||||
cijFkALeQp/qyeXdFld2v9gUN3eCgljgcl0QweRoIc=---`)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte(`{
|
||||
"new-authz": "https://foo/acme/new-authz",
|
||||
"new-cert": "https://foo/acme/new-cert",
|
||||
"new-reg": "https://foo/acme/new-reg",
|
||||
"revoke-cert": "https://foo/acme/revoke-cert"
|
||||
}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
a := ACME{DNSProvider: "manual", DelayDontCheckDNS: 10, CAServer: ts.URL}
|
||||
|
||||
client, err := a.buildACMEClient(account)
|
||||
if err != nil {
|
||||
t.Errorf("Error in buildACMEClient: %v", err)
|
||||
}
|
||||
if client == nil {
|
||||
t.Errorf("No client from buildACMEClient!")
|
||||
}
|
||||
if acme.PreCheckDNS == nil {
|
||||
t.Errorf("No change to acme.PreCheckDNS when meant to be adding enforcing override function.")
|
||||
}
|
||||
}
|
||||
|
@@ -2,15 +2,16 @@ package acme
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
"github.com/cenk/backoff"
|
||||
"github.com/containous/traefik/cluster"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/xenolf/lego/acme"
|
||||
"time"
|
||||
)
|
||||
|
||||
var _ acme.ChallengeProviderTimeout = (*challengeProvider)(nil)
|
||||
@@ -49,7 +50,7 @@ func (c *challengeProvider) getCertificate(domain string) (cert *tls.Certificate
|
||||
}
|
||||
ebo := backoff.NewExponentialBackOff()
|
||||
ebo.MaxElapsedTime = 60 * time.Second
|
||||
err := backoff.RetryNotify(operation, ebo, notify)
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify)
|
||||
if err != nil {
|
||||
log.Errorf("Error getting cert: %v", err)
|
||||
return nil, false
|
||||
|
@@ -17,34 +17,44 @@ import (
|
||||
)
|
||||
|
||||
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)
|
||||
_, 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)
|
||||
|
||||
certPEM, keyPEM, err := generateKeyPair(domain, time.Time{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM)
|
||||
certificate, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
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)
|
||||
|
||||
func generateKeyPair(domain string, expiration time.Time) ([]byte, []byte, error) {
|
||||
rsaPrivKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(rsaPrivKey)})
|
||||
|
||||
certPEM, err := generatePemCert(rsaPrivKey, domain, expiration)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
func generatePemCert(privKey *rsa.PrivateKey, domain string, expiration time.Time) ([]byte, error) {
|
||||
derBytes, err := generateDerCert(privKey, expiration, domain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -93,7 +103,7 @@ func TLSSNI01ChallengeCert(keyAuth string) (ChallengeCert, string, error) {
|
||||
zBytes := sha256.Sum256([]byte(keyAuth))
|
||||
z := hex.EncodeToString(zBytes[:sha256.Size])
|
||||
domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:])
|
||||
tempCertPEM, err := generatePemCert(rsaPrivKey, domain)
|
||||
tempCertPEM, err := generatePemCert(rsaPrivKey, domain, time.Time{})
|
||||
if err != nil {
|
||||
return ChallengeCert{}, "", err
|
||||
}
|
||||
|
@@ -3,10 +3,12 @@ package acme
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/containous/traefik/cluster"
|
||||
"github.com/containous/traefik/log"
|
||||
"io/ioutil"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var _ cluster.Store = (*LocalStore)(nil)
|
||||
@@ -37,7 +39,17 @@ func (s *LocalStore) Load() (cluster.Object, error) {
|
||||
s.storageLock.Lock()
|
||||
defer s.storageLock.Unlock()
|
||||
account := &Account{}
|
||||
file, err := ioutil.ReadFile(s.file)
|
||||
|
||||
err := checkPermissions(s.file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, err := os.Open(s.file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
file, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
25
acme/localStore_unix.go
Normal file
25
acme/localStore_unix.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// +build !windows
|
||||
|
||||
package acme
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Check file permissions
|
||||
func checkPermissions(name string) error {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.Mode().Perm()&0077 != 0 {
|
||||
return fmt.Errorf("permissions %o for %s are too open, please use 600", fi.Mode().Perm(), name)
|
||||
}
|
||||
return nil
|
||||
}
|
6
acme/localStore_windows.go
Normal file
6
acme/localStore_windows.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package acme
|
||||
|
||||
// Do not check file permissions on Windows right now
|
||||
func checkPermissions(name string) error {
|
||||
return nil
|
||||
}
|
@@ -1,25 +1,36 @@
|
||||
FROM golang:1.7
|
||||
|
||||
RUN go get github.com/Masterminds/glide \
|
||||
&& go get github.com/jteeuwen/go-bindata/... \
|
||||
RUN go get github.com/jteeuwen/go-bindata/... \
|
||||
&& go get github.com/golang/lint/golint \
|
||||
&& go get github.com/kisielk/errcheck
|
||||
&& go get github.com/kisielk/errcheck \
|
||||
&& go get github.com/client9/misspell/cmd/misspell \
|
||||
&& go get github.com/mattfarina/glide-hash
|
||||
|
||||
# Which docker version to test on
|
||||
ARG DOCKER_VERSION=1.10.1
|
||||
ARG DOCKER_VERSION=1.10.3
|
||||
|
||||
|
||||
# Which glide version to test on
|
||||
ARG GLIDE_VERSION=v0.12.3
|
||||
|
||||
# Download glide
|
||||
RUN mkdir -p /usr/local/bin \
|
||||
&& curl -fL https://github.com/Masterminds/glide/releases/download/${GLIDE_VERSION}/glide-${GLIDE_VERSION}-linux-amd64.tar.gz \
|
||||
| tar -xzC /usr/local/bin --transform 's#^.+/##x'
|
||||
|
||||
# Download docker
|
||||
RUN set -ex; \
|
||||
curl https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION} -o /usr/local/bin/docker-${DOCKER_VERSION}; \
|
||||
chmod +x /usr/local/bin/docker-${DOCKER_VERSION}
|
||||
|
||||
# Set the default Docker to be run
|
||||
RUN ln -s /usr/local/bin/docker-${DOCKER_VERSION} /usr/local/bin/docker
|
||||
RUN mkdir -p /usr/local/bin \
|
||||
&& curl -fL https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION}.tgz \
|
||||
| tar -xzC /usr/local/bin --transform 's#^.+/##x'
|
||||
|
||||
WORKDIR /go/src/github.com/containous/traefik
|
||||
|
||||
COPY glide.yaml glide.yaml
|
||||
COPY glide.lock glide.lock
|
||||
RUN glide install
|
||||
RUN glide install -v
|
||||
|
||||
COPY integration/glide.yaml integration/glide.yaml
|
||||
COPY integration/glide.lock integration/glide.lock
|
||||
RUN cd integration && glide install
|
||||
|
||||
COPY . /go/src/github.com/containous/traefik
|
||||
|
@@ -1,17 +1,19 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cenk/backoff"
|
||||
"github.com/containous/staert"
|
||||
"github.com/containous/traefik/job"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/satori/go.uuid"
|
||||
"golang.org/x/net/context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Metadata stores Object plus metadata
|
||||
@@ -108,7 +110,7 @@ func (d *Datastore) watchChanges() error {
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Error in watch datastore: %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
log.Errorf("Error in watch datastore: %v", err)
|
||||
}
|
||||
@@ -175,7 +177,7 @@ func (d *Datastore) Begin() (Transaction, Object, error) {
|
||||
}
|
||||
ebo := backoff.NewExponentialBackOff()
|
||||
ebo.MaxElapsedTime = 60 * time.Second
|
||||
err = backoff.RetryNotify(operation, ebo, notify)
|
||||
err = backoff.RetryNotify(safe.OperationWithRecover(operation), ebo, notify)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Datastore cannot sync: %v", err)
|
||||
}
|
||||
@@ -230,24 +232,24 @@ func (s *datastoreTransaction) Commit(object Object) error {
|
||||
s.localLock.Lock()
|
||||
defer s.localLock.Unlock()
|
||||
if s.dirty {
|
||||
return fmt.Errorf("transaction already used, please begin a new one")
|
||||
return fmt.Errorf("Transaction already used, please begin a new one")
|
||||
}
|
||||
s.Datastore.meta.object = object
|
||||
err := s.Datastore.meta.Marshall()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("Marshall error: %s", err)
|
||||
}
|
||||
err = s.kv.StoreConfig(s.Datastore.meta)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("StoreConfig error: %s", err)
|
||||
}
|
||||
|
||||
err = s.remoteLock.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("Unlock error: %s", err)
|
||||
}
|
||||
|
||||
s.dirty = true
|
||||
log.Debugf("Transaction commited %s", s.id)
|
||||
log.Debugf("Transaction committed %s", s.id)
|
||||
return nil
|
||||
}
|
||||
|
@@ -1,13 +1,14 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/cenk/backoff"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/leadership"
|
||||
"golang.org/x/net/context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Leadership allows leadership election using a KV store
|
||||
@@ -15,7 +16,7 @@ type Leadership struct {
|
||||
*safe.Pool
|
||||
*types.Cluster
|
||||
candidate *leadership.Candidate
|
||||
leader safe.Safe
|
||||
leader *safe.Safe
|
||||
listeners []LeaderListener
|
||||
}
|
||||
|
||||
@@ -26,6 +27,7 @@ func NewLeadership(ctx context.Context, cluster *types.Cluster) *Leadership {
|
||||
Cluster: cluster,
|
||||
candidate: leadership.NewCandidate(cluster.Store, cluster.Store.Prefix+"/leader", cluster.Node, 20*time.Second),
|
||||
listeners: []LeaderListener{},
|
||||
leader: safe.New(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +47,7 @@ func (l *Leadership) Participate(pool *safe.Pool) {
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Leadership election error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, backOff, notify)
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), backOff, notify)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot elect leadership %+v", err)
|
||||
}
|
||||
|
111
cmd/bug.go
Normal file
111
cmd/bug.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"text/template"
|
||||
|
||||
"github.com/containous/flaeg"
|
||||
"github.com/mvdan/xurls"
|
||||
)
|
||||
|
||||
var (
|
||||
bugtracker = "https://github.com/containous/traefik/issues/new"
|
||||
bugTemplate = `### What version of Traefik are you using?
|
||||
` + "```" + `
|
||||
{{.Version}}
|
||||
` + "```" + `
|
||||
|
||||
### What is your environment & configuration (arguments, toml...)?
|
||||
` + "```" + `
|
||||
{{.Configuration}}
|
||||
` + "```" + `
|
||||
|
||||
### What did you do?
|
||||
|
||||
|
||||
### What did you expect to see?
|
||||
|
||||
|
||||
### What did you see instead?
|
||||
`
|
||||
)
|
||||
|
||||
// NewBugCmd builds a new Bug command
|
||||
func NewBugCmd(traefikConfiguration interface{}, traefikPointersConfiguration interface{}) *flaeg.Command {
|
||||
|
||||
//version Command init
|
||||
return &flaeg.Command{
|
||||
Name: "bug",
|
||||
Description: `Report an issue on Traefik bugtracker`,
|
||||
Config: traefikConfiguration,
|
||||
DefaultPointersConfig: traefikPointersConfiguration,
|
||||
Run: func() error {
|
||||
var version bytes.Buffer
|
||||
if err := getVersionPrint(&version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tmpl, err := template.New("").Parse(bugTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configJSON, err := json.MarshalIndent(traefikConfiguration, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := struct {
|
||||
Version string
|
||||
Configuration string
|
||||
}{
|
||||
Version: version.String(),
|
||||
Configuration: anonymize(string(configJSON)),
|
||||
}
|
||||
|
||||
var bug bytes.Buffer
|
||||
if err := tmpl.Execute(&bug, v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := bug.String()
|
||||
url := bugtracker + "?body=" + url.QueryEscape(body)
|
||||
if err := openBrowser(url); err != nil {
|
||||
fmt.Print("Please file a new issue at " + bugtracker + " using this template:\n\n")
|
||||
fmt.Print(body)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Metadata: map[string]string{
|
||||
"parseAllSources": "true",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func openBrowser(url string) error {
|
||||
var err error
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
err = exec.Command("xdg-open", url).Start()
|
||||
case "windows":
|
||||
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
case "darwin":
|
||||
err = exec.Command("open", url).Start()
|
||||
default:
|
||||
err = fmt.Errorf("unsupported platform")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func anonymize(input string) string {
|
||||
replace := "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
|
||||
mailExp := regexp.MustCompile(`\w[-._\w]*\w@\w[-._\w]*\w\.\w{2,3}"`)
|
||||
return xurls.Relaxed.ReplaceAllString(mailExp.ReplaceAllString(input, replace), replace)
|
||||
}
|
63
cmd/version.go
Normal file
63
cmd/version.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"text/template"
|
||||
|
||||
"github.com/containous/flaeg"
|
||||
"github.com/containous/traefik/version"
|
||||
)
|
||||
|
||||
var versionTemplate = `Version: {{.Version}}
|
||||
Codename: {{.Codename}}
|
||||
Go version: {{.GoVersion}}
|
||||
Built: {{.BuildTime}}
|
||||
OS/Arch: {{.Os}}/{{.Arch}}`
|
||||
|
||||
// NewVersionCmd builds a new Version command
|
||||
func NewVersionCmd() *flaeg.Command {
|
||||
|
||||
//version Command init
|
||||
return &flaeg.Command{
|
||||
Name: "version",
|
||||
Description: `Print version`,
|
||||
Config: struct{}{},
|
||||
DefaultPointersConfig: struct{}{},
|
||||
Run: func() error {
|
||||
if err := getVersionPrint(os.Stdout); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
return nil
|
||||
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getVersionPrint(wr io.Writer) error {
|
||||
tmpl, err := template.New("").Parse(versionTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
v := struct {
|
||||
Version string
|
||||
Codename string
|
||||
GoVersion string
|
||||
BuildTime string
|
||||
Os string
|
||||
Arch string
|
||||
}{
|
||||
Version: version.Version,
|
||||
Codename: version.Codename,
|
||||
GoVersion: runtime.Version(),
|
||||
BuildTime: version.BuildDate,
|
||||
Os: runtime.GOOS,
|
||||
Arch: runtime.GOARCH,
|
||||
}
|
||||
|
||||
return tmpl.Execute(wr, v)
|
||||
}
|
@@ -49,6 +49,9 @@ type GlobalConfiguration struct {
|
||||
Boltdb *provider.BoltDb `description:"Enable Boltdb backend"`
|
||||
Kubernetes *provider.Kubernetes `description:"Enable Kubernetes backend"`
|
||||
Mesos *provider.Mesos `description:"Enable Mesos backend"`
|
||||
Eureka *provider.Eureka `description:"Enable Eureka backend"`
|
||||
ECS *provider.ECS `description:"Enable ECS backend"`
|
||||
Rancher *provider.Rancher `description:"Enable Rancher backend"`
|
||||
}
|
||||
|
||||
// DefaultEntryPoints holds default entry points
|
||||
@@ -323,6 +326,16 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
|
||||
// default Web
|
||||
var defaultWeb WebProvider
|
||||
defaultWeb.Address = ":8080"
|
||||
defaultWeb.Statistics = &types.Statistics{
|
||||
RecentErrors: 10,
|
||||
}
|
||||
|
||||
// default Metrics
|
||||
defaultWeb.Metrics = &types.Metrics{
|
||||
Prometheus: &types.Prometheus{
|
||||
Buckets: types.Buckets{0.1, 0.3, 1.2, 5},
|
||||
},
|
||||
}
|
||||
|
||||
// default Marathon
|
||||
var defaultMarathon provider.Marathon
|
||||
@@ -330,6 +343,8 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
|
||||
defaultMarathon.Endpoint = "http://127.0.0.1:8080"
|
||||
defaultMarathon.ExposedByDefault = true
|
||||
defaultMarathon.Constraints = types.Constraints{}
|
||||
defaultMarathon.DialerTimeout = 60
|
||||
defaultMarathon.KeepAlive = 10
|
||||
|
||||
// default Consul
|
||||
var defaultConsul provider.Consul
|
||||
@@ -377,6 +392,22 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
|
||||
defaultMesos.Endpoint = "http://127.0.0.1:5050"
|
||||
defaultMesos.ExposedByDefault = true
|
||||
defaultMesos.Constraints = types.Constraints{}
|
||||
defaultMesos.RefreshSeconds = 30
|
||||
defaultMesos.ZkDetectionTimeout = 30
|
||||
defaultMesos.StateTimeoutSecond = 30
|
||||
|
||||
//default ECS
|
||||
var defaultECS provider.ECS
|
||||
defaultECS.Watch = true
|
||||
defaultECS.ExposedByDefault = true
|
||||
defaultECS.RefreshSeconds = 15
|
||||
defaultECS.Cluster = "default"
|
||||
defaultECS.Constraints = types.Constraints{}
|
||||
|
||||
//default Rancher
|
||||
var defaultRancher provider.Rancher
|
||||
defaultRancher.Watch = true
|
||||
defaultRancher.ExposedByDefault = true
|
||||
|
||||
defaultConfiguration := GlobalConfiguration{
|
||||
Docker: &defaultDocker,
|
||||
@@ -390,8 +421,14 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
|
||||
Boltdb: &defaultBoltDb,
|
||||
Kubernetes: &defaultKubernetes,
|
||||
Mesos: &defaultMesos,
|
||||
ECS: &defaultECS,
|
||||
Rancher: &defaultRancher,
|
||||
Retry: &Retry{},
|
||||
}
|
||||
|
||||
//default Rancher
|
||||
//@TODO: ADD
|
||||
|
||||
return &TraefikConfiguration{
|
||||
GlobalConfiguration: defaultConfiguration,
|
||||
}
|
||||
|
@@ -4,7 +4,8 @@ Description=Traefik
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/traefik --configFile=/etc/traefik.toml
|
||||
Restart=on-failure
|
||||
Restart=always
|
||||
WatchdogSec=1s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
10
docs.Dockerfile
Normal file
10
docs.Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM alpine:3.7
|
||||
|
||||
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/bin
|
||||
|
||||
COPY requirements.txt /mkdocs/
|
||||
WORKDIR /mkdocs
|
||||
VOLUME /mkdocs
|
||||
|
||||
RUN apk --no-cache --no-progress add py-pip \
|
||||
&& pip install --trusted-host pypi.python.org --user -r requirements.txt
|
@@ -85,8 +85,10 @@ Frontends can be defined using the following rules:
|
||||
- `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.
|
||||
- `AddPrefix` : Add prefix from the request URL's Path.
|
||||
|
||||
You can use multiple rules by separating them by `;`
|
||||
You can use multlple values for a rule by separating them with `,`.
|
||||
You can use multiple rules by separating them by `;`.
|
||||
|
||||
You can optionally enable `passHostHeader` to forward client `Host` header to the backend.
|
||||
|
||||
@@ -104,7 +106,7 @@ Here is an example of frontends definition:
|
||||
priority = 10
|
||||
entrypoints = ["https"] # overrides defaultEntryPoints
|
||||
[frontends.frontend2.routes.test_1]
|
||||
rule = "Host:localhost,{subdomain:[a-z]+}.localhost"
|
||||
rule = "HostRegexp:localhost,{subdomain:[a-z]+}.localhost"
|
||||
[frontends.frontend3]
|
||||
backend = "backend2"
|
||||
[frontends.frontend3.routes.test_1]
|
||||
@@ -180,16 +182,16 @@ Here, `frontend1` will be matched before `frontend2` (`10 > 5`).
|
||||
## 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:
|
||||
Various methods of load-balancing are 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.
|
||||
In case the condition matches, CB enters Tripped state, where it responds with predefined code or redirects to another frontend.
|
||||
Once Tripped timer expires, CB enters Recovering state and resets all stats.
|
||||
In case if the condition does not match and recovery timer expires, CB enters Standby state.
|
||||
In case the condition does not match and recovery timer expires, CB enters Standby state.
|
||||
|
||||
It can be configured using:
|
||||
|
||||
@@ -233,6 +235,26 @@ For example:
|
||||
[backends.backend1.loadbalancer]
|
||||
sticky = true
|
||||
```
|
||||
|
||||
A health check can be configured in order to remove a backend from LB rotation
|
||||
as long as it keeps returning HTTP status codes other than 200 OK to HTTP GET
|
||||
requests periodically carried out by Traefik. The check is defined by a path
|
||||
appended to the backend URL and an interval (given in a format understood by [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration)) specifying how
|
||||
often the health check should be executed (the default being 30 seconds). Each
|
||||
backend must respond to the health check within 5 seconds.
|
||||
|
||||
A recovering backend returning 200 OK responses again is being returned to the
|
||||
LB rotation pool.
|
||||
|
||||
For example:
|
||||
```toml
|
||||
[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.healthcheck]
|
||||
path = "/health"
|
||||
interval = "10s"
|
||||
```
|
||||
|
||||
## Servers
|
||||
|
||||
Servers are simply defined using a `URL`. You can also apply a custom `weight` to each server (this will be used by load-balancing).
|
||||
@@ -270,13 +292,13 @@ Here is an example of backends and servers definition:
|
||||
|
||||
Træfɪk's configuration has two parts:
|
||||
|
||||
- The [static Træfɪk configuration](/basics#static-trfk-configuration) which is loaded only at the begining.
|
||||
- The [static Træfɪk configuration](/basics#static-trfk-configuration) which is loaded only at the beginning.
|
||||
- The [dynamic Træfɪk configuration](/basics#dynamic-trfk-configuration) which can be hot-reloaded (no need to restart the process).
|
||||
|
||||
|
||||
## Static Træfɪk configuration
|
||||
|
||||
The static configuration is the global configuration which setting up connections to configuration backends and entrypoints.
|
||||
The static configuration is the global configuration which is setting up connections to configuration backends and entrypoints.
|
||||
|
||||
Træfɪk can be configured using many configuration sources with the following precedence order.
|
||||
Each item takes precedence over the item below it:
|
||||
@@ -286,7 +308,7 @@ Each item takes precedence over the item below it:
|
||||
- [Configuration file](/basics/#configuration-file)
|
||||
- Default
|
||||
|
||||
It means that arguments overrides configuration file, and Key-value Store overrides arguments.
|
||||
It means that arguments override configuration file, and Key-value Store overrides arguments.
|
||||
|
||||
### Configuration file
|
||||
|
||||
@@ -356,7 +378,7 @@ All those related flags will be displayed with :
|
||||
$ traefik [command] --help
|
||||
```
|
||||
|
||||
Note that each command is described at the begining of the help section:
|
||||
Note that each command is described at the beginning of the help section:
|
||||
|
||||
```bash
|
||||
$ traefik --help
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
@@ -11,7 +11,7 @@
|
||||
|
||||
|
||||
Træfɪk is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
|
||||
It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), Rest API, file...) to manage its configuration automatically and dynamically.
|
||||
It supports several backends ([Docker](https://www.docker.com/), [Swarm](https://docs.docker.com/swarm), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Consul](https://www.consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), [Amazon ECS](https://aws.amazon.com/ecs/), Rest API, file...) to manage its configuration automatically and dynamically.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -34,7 +34,7 @@ Træfɪk can listen to your service registry/orchestrator API, and knows each ti
|
||||
Routes to your services will be created instantly.
|
||||
|
||||
Run it and forget it!
|
||||
|
||||
|
||||
|
||||
## Quickstart
|
||||
|
||||
@@ -70,39 +70,63 @@ docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.to
|
||||
|
||||
## Test it
|
||||
|
||||
You can test Træfɪk easily using [Docker compose](https://docs.docker.com/compose), with this `docker-compose.yml` file:
|
||||
You can test Træfɪk easily using [Docker compose](https://docs.docker.com/compose), with this `docker-compose.yml` file in a folder named `traefik`:
|
||||
|
||||
```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
|
||||
version: '2'
|
||||
|
||||
whoami1:
|
||||
image: emilevauge/whoami
|
||||
labels:
|
||||
- "traefik.backend=whoami"
|
||||
- "traefik.frontend.rule=Host:whoami.docker.localhost"
|
||||
services:
|
||||
proxy:
|
||||
image: traefik
|
||||
command: --web --docker --docker.domain=docker.localhost --logLevel=DEBUG
|
||||
networks:
|
||||
- webgateway
|
||||
ports:
|
||||
- "80:80"
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /dev/null:/traefik.toml
|
||||
|
||||
whoami2:
|
||||
image: emilevauge/whoami
|
||||
labels:
|
||||
- "traefik.backend=whoami"
|
||||
- "traefik.frontend.rule=Host:whoami.docker.localhost"
|
||||
networks:
|
||||
webgateway:
|
||||
driver: bridge
|
||||
```
|
||||
|
||||
Then, start it:
|
||||
|
||||
Start it from within the `traefik` folder:
|
||||
|
||||
docker-compose up -d
|
||||
|
||||
In a browser you may open `http://localhost:8080` to access Træfɪk's dashboard and observe the following magic.
|
||||
|
||||
Now, create a folder named `test` and create a `docker-compose.yml` in it with this content:
|
||||
|
||||
```yaml
|
||||
version: '2'
|
||||
|
||||
services:
|
||||
whoami:
|
||||
image: emilevauge/whoami
|
||||
networks:
|
||||
- web
|
||||
labels:
|
||||
- "traefik.backend=whoami"
|
||||
- "traefik.frontend.rule=Host:whoami.docker.localhost"
|
||||
|
||||
networks:
|
||||
web:
|
||||
external:
|
||||
name: traefik_webgateway
|
||||
```
|
||||
|
||||
Then, start and scale it in the `test` folder:
|
||||
|
||||
```
|
||||
docker-compose up -d
|
||||
docker-compose scale whoami=2
|
||||
```
|
||||
|
||||
Finally, test load-balancing between the two servers `whoami1` and `whoami2`:
|
||||
Finally, test load-balancing between the two services `test_whoami_1` and `test_whoami_2`:
|
||||
|
||||
```bash
|
||||
$ curl -H Host:whoami.docker.localhost http://127.0.0.1
|
||||
|
331
docs/toml.md
331
docs/toml.md
@@ -62,11 +62,13 @@
|
||||
#
|
||||
# ProvidersThrottleDuration = "5"
|
||||
|
||||
# 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.
|
||||
# Controls the maximum idle (keep-alive) connections to keep per-host. If zero, DefaultMaxIdleConnsPerHost
|
||||
# from the Go standard library net/http module is used.
|
||||
# If you encounter 'too many open files' errors, you can either increase this
|
||||
# value or change the `ulimit`.
|
||||
#
|
||||
# Optional
|
||||
# Default: http.DefaultMaxIdleConnsPerHost
|
||||
# Default: 200
|
||||
#
|
||||
# MaxIdleConnsPerHost = 200
|
||||
|
||||
@@ -282,13 +284,50 @@ email = "test@traefik.io"
|
||||
#
|
||||
storage = "acme.json" # or "traefik/acme/account" if using KV store
|
||||
|
||||
# Entrypoint to proxy acme challenge to.
|
||||
# Entrypoint to proxy acme challenge/apply certificates to.
|
||||
# WARNING, must point to an entrypoint on port 443
|
||||
#
|
||||
# Required
|
||||
#
|
||||
entryPoint = "https"
|
||||
|
||||
# Use a DNS based acme challenge rather than external HTTPS access, e.g. for a firewalled server
|
||||
# Select the provider that matches the DNS domain that will host the challenge TXT record,
|
||||
# and provide environment variables with access keys to enable setting it:
|
||||
# - cloudflare: CLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY
|
||||
# - digitalocean: DO_AUTH_TOKEN
|
||||
# - dnsimple: DNSIMPLE_EMAIL, DNSIMPLE_API_KEY
|
||||
# - dnsmadeeasy: DNSMADEEASY_API_KEY, DNSMADEEASY_API_SECRET
|
||||
# - exoscale: EXOSCALE_API_KEY, EXOSCALE_API_SECRET
|
||||
# - gandi: GANDI_API_KEY
|
||||
# - linode: LINODE_API_KEY
|
||||
# - manual: none, but run traefik interactively & turn on acmeLogging to see instructions & press Enter
|
||||
# - namecheap: NAMECHEAP_API_USER, NAMECHEAP_API_KEY
|
||||
# - rfc2136: RFC2136_TSIG_KEY, RFC2136_TSIG_SECRET, RFC2136_TSIG_ALGORITHM, RFC2136_NAMESERVER
|
||||
# - route53: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, or configured user/instance IAM profile
|
||||
# - dyn: DYN_CUSTOMER_NAME, DYN_USER_NAME, DYN_PASSWORD
|
||||
# - vultr: VULTR_API_KEY
|
||||
# - ovh: OVH_ENDPOINT, OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY
|
||||
# - pdns: PDNS_API_KEY, PDNS_API_URL
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# dnsProvider = "digitalocean"
|
||||
|
||||
# By default, the dnsProvider will verify the TXT DNS challenge record before letting ACME verify
|
||||
# If delayDontCheckDNS is greater than zero, avoid this & instead just wait so many seconds.
|
||||
# Useful if internal networks block external DNS queries
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# delayDontCheckDNS = 0
|
||||
|
||||
# If true, display debug log messages from the acme client library
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# acmeLogging = true
|
||||
|
||||
# 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://letsencrypt.org/docs/rate-limits
|
||||
@@ -503,6 +542,14 @@ address = ":8080"
|
||||
# Optional
|
||||
# ReadOnly = false
|
||||
#
|
||||
# To enable more detailed statistics
|
||||
# [web.statistics]
|
||||
# RecentErrors = 10
|
||||
#
|
||||
# To enable Traefik to export internal metrics to Prometheus
|
||||
# [web.metrics.prometheus]
|
||||
# Buckets=[0.1,0.3,1.2,5]
|
||||
#
|
||||
# To enable basic auth on the webui
|
||||
# with 2 user/pass: test:test and test2:test2
|
||||
# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones
|
||||
@@ -577,7 +624,26 @@ $ curl -s "http://localhost:8080/health" | jq .
|
||||
// average response time (formated time)
|
||||
"average_response_time": "864.8016ms",
|
||||
// average response time in seconds
|
||||
"average_response_time_sec": 0.8648016000000001
|
||||
"average_response_time_sec": 0.8648016000000001,
|
||||
|
||||
// request statistics [requires --web.statistics to be set]
|
||||
// ten most recent requests with 4xx and 5xx status codes
|
||||
"recent_errors": [
|
||||
{
|
||||
// status code
|
||||
"status_code": 500,
|
||||
// description of status code
|
||||
"status": "Internal Server Error",
|
||||
// request HTTP method
|
||||
"method": "GET",
|
||||
// request hostname
|
||||
"host": "localhost",
|
||||
// request path
|
||||
"path": "/path",
|
||||
// RFC 3339 formatted date/time
|
||||
"time": "2016-10-21T16:59:15.418495872-07:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -655,6 +721,11 @@ $ curl -s "http://localhost:8080/api" | jq .
|
||||
- `/api/providers/{provider}/frontends/{frontend}/routes`: `GET` routes in a frontend
|
||||
- `/api/providers/{provider}/frontends/{frontend}/routes/{route}`: `GET` a route in a frontend
|
||||
|
||||
- `/metrics`: You can enable Traefik to export internal metrics to different monitoring systems (Only Prometheus is supported at the moment).
|
||||
|
||||
```bash
|
||||
$ traefik --web.metrics.prometheus --web.metrics.prometheus.buckets="0.1,0.3,1.2,5"
|
||||
```
|
||||
|
||||
## Docker backend
|
||||
|
||||
@@ -697,6 +768,7 @@ watch = true
|
||||
# filename = "docker.tmpl"
|
||||
|
||||
# Expose containers by default in traefik
|
||||
# If set to false, containers that don't have `traefik.enable=true` will be ignored
|
||||
#
|
||||
# Optional
|
||||
# Default: true
|
||||
@@ -734,6 +806,7 @@ Labels can be used on containers to override default behaviour:
|
||||
- `traefik.backend.maxconn.extractorfunc=client.ip`: set the function to be used against the request to determine what to limit maximum connections to the backend by. Must be used in conjunction with the above label to take effect.
|
||||
- `traefik.backend.loadbalancer.method=drr`: override the default `wrr` load balancer algorithm
|
||||
- `traefik.backend.loadbalancer.sticky=true`: enable backend sticky sessions
|
||||
- `traefik.backend.loadbalancer.swarm=true `: use Swarm's inbuilt load balancer (only relevant under Swarm Mode).
|
||||
- `traefik.backend.circuitbreaker.expression=NetworkErrorRatio() > 0.5`: create a [circuit breaker](/basics/#backends) to be used against the backend
|
||||
- `traefik.port=80`: register this port. Useful when the container exposes multiples ports.
|
||||
- `traefik.protocol=https`: override the default `http` protocol
|
||||
@@ -743,7 +816,7 @@ Labels can be used on containers to override default behaviour:
|
||||
- `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend.
|
||||
- `traefik.frontend.priority=10`: override default frontend priority
|
||||
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
|
||||
- `traefik.docker.network`: Set the docker network to use for connections to this container
|
||||
- `traefik.docker.network`: Set the docker network to use for connections to this container. If a container is linked to several networks, be sure to set the proper network name (you can check with docker inspect <container_id>) otherwise it will randomly pick one (depending on how docker is returning them). For instance when deploying docker `stack` from compose files, the compose defined networks will be prefixed with the `stack` name.
|
||||
|
||||
NB: when running inside a container, Træfɪk will need network access through `docker network connect <network> <traefik-container>`
|
||||
|
||||
@@ -792,7 +865,7 @@ domain = "marathon.localhost"
|
||||
# Expose Marathon apps by default in traefik
|
||||
#
|
||||
# Optional
|
||||
# Default: false
|
||||
# Default: true
|
||||
#
|
||||
# exposedByDefault = true
|
||||
|
||||
@@ -825,6 +898,9 @@ domain = "marathon.localhost"
|
||||
# Optional
|
||||
#
|
||||
# [marathon.TLS]
|
||||
# CA = "/etc/ssl/ca.crt"
|
||||
# Cert = "/etc/ssl/marathon.cert"
|
||||
# Key = "/etc/ssl/marathon.key"
|
||||
# InsecureSkipVerify = true
|
||||
|
||||
# DCOSToken for DCOS environment, This will override the Authorization header
|
||||
@@ -832,6 +908,21 @@ domain = "marathon.localhost"
|
||||
# Optional
|
||||
#
|
||||
# dcosToken = "xxxxxx"
|
||||
|
||||
# Override DialerTimeout
|
||||
# Amount of time in seconds to allow the Marathon provider to wait to open a TCP
|
||||
# connection to a Marathon master
|
||||
#
|
||||
# Optional
|
||||
# Default: 60
|
||||
# dialerTimeout = 5
|
||||
|
||||
# Set the TCP Keep Alive interval (in seconds) for the Marathon HTTP Client
|
||||
#
|
||||
# Optional
|
||||
# Default: 10
|
||||
#
|
||||
# keepAlive = 10
|
||||
```
|
||||
|
||||
Labels can be used on containers to override default behaviour:
|
||||
@@ -914,12 +1005,14 @@ domain = "mesos.localhost"
|
||||
# Zookeeper timeout (in seconds)
|
||||
#
|
||||
# Optional
|
||||
# Default: 30
|
||||
#
|
||||
# ZkDetectionTimeout = 30
|
||||
|
||||
# Polling interval (in seconds)
|
||||
#
|
||||
# Optional
|
||||
# Default: 30
|
||||
#
|
||||
# RefreshSeconds = 30
|
||||
|
||||
@@ -932,8 +1025,9 @@ domain = "mesos.localhost"
|
||||
# HTTP Timeout (in seconds)
|
||||
#
|
||||
# Optional
|
||||
# Default: 30
|
||||
#
|
||||
# StateTimeoutSecond = "host"
|
||||
# StateTimeoutSecond = "30"
|
||||
```
|
||||
|
||||
## Kubernetes Ingress backend
|
||||
@@ -973,8 +1067,17 @@ Annotations can be used on containers to override default behaviour for the whol
|
||||
|
||||
- `traefik.frontend.rule.type: PathPrefixStrip`: override the default frontend rule type (Default: `PathPrefix`).
|
||||
|
||||
Annotations can be used on the Kubernetes service to override default behaviour:
|
||||
|
||||
- `traefik.backend.loadbalancer.method=drr`: override the default `wrr` load balancer algorithm
|
||||
- `traefik.backend.loadbalancer.sticky=true`: enable backend sticky sessions
|
||||
|
||||
You can find here an example [ingress](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/cheese-ingress.yaml) and [replication controller](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/traefik.yaml).
|
||||
|
||||
Additionally, an annotation can be used on Kubernetes services to set the [circuit breaker expression](https://docs.traefik.io/basics/#backends) for a backend.
|
||||
|
||||
- `traefik.backend.circuitbreaker: <expression>`: set the circuit breaker expression for the backend (Default: nil).
|
||||
|
||||
## Consul backend
|
||||
|
||||
Træfɪk can be configured to use Consul as a backend configuration:
|
||||
@@ -1162,7 +1265,7 @@ watch = true
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
prefix = "/traefik"
|
||||
prefix = "traefik"
|
||||
|
||||
# Override default configuration template. For advanced users :)
|
||||
#
|
||||
@@ -1213,4 +1316,214 @@ prefix = "/traefik"
|
||||
# filename = "boltdb.tmpl"
|
||||
```
|
||||
|
||||
## Eureka backend
|
||||
|
||||
Træfɪk can be configured to use Eureka as a backend configuration:
|
||||
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# Eureka configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable Eureka configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[eureka]
|
||||
|
||||
# Eureka server endpoint.
|
||||
# endpoint := "http://my.eureka.server/eureka"
|
||||
#
|
||||
# Required
|
||||
#
|
||||
endpoint = "http://my.eureka.server/eureka"
|
||||
|
||||
# Override default configuration time between refresh
|
||||
#
|
||||
# Optional
|
||||
# default 30s
|
||||
delay = "1m"
|
||||
|
||||
# Override default configuration template. For advanced users :)
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# filename = "eureka.tmpl"
|
||||
```
|
||||
|
||||
Please refer to the [Key Value storage structure](/user-guide/kv-config/#key-value-storage-structure) section to get documentation on traefik KV structure.
|
||||
|
||||
|
||||
## ECS backend
|
||||
|
||||
Træfɪk can be configured to use Amazon ECS as a backend configuration:
|
||||
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# ECS configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable ECS configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[ecs]
|
||||
|
||||
# ECS Cluster Name
|
||||
#
|
||||
# Optional
|
||||
# Default: "default"
|
||||
#
|
||||
Cluster = "default"
|
||||
|
||||
# Enable watch ECS changes
|
||||
#
|
||||
# Optional
|
||||
# Default: true
|
||||
#
|
||||
Watch = true
|
||||
|
||||
# Polling interval (in seconds)
|
||||
#
|
||||
# Optional
|
||||
# Default: 15
|
||||
#
|
||||
RefreshSeconds = 15
|
||||
|
||||
# Expose ECS services by default in traefik
|
||||
#
|
||||
# Optional
|
||||
# Default: true
|
||||
#
|
||||
ExposedByDefault = false
|
||||
|
||||
# Region to use when connecting to AWS
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# Region = "us-east-1"
|
||||
|
||||
# AccessKeyID to use when connecting to AWS
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# AccessKeyID = "abc"
|
||||
|
||||
# SecretAccessKey to use when connecting to AWS
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
# SecretAccessKey = "123"
|
||||
|
||||
```
|
||||
|
||||
Labels can be used on task containers to override default behaviour:
|
||||
|
||||
- `traefik.protocol=https`: override the default `http` protocol
|
||||
- `traefik.weight=10`: assign this weight to the container
|
||||
- `traefik.enable=false`: disable this container in Træfɪk
|
||||
- `traefik.frontend.rule=Host:test.traefik.io`: override the default frontend rule (Default: `Host:{containerName}.{domain}`).
|
||||
- `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend.
|
||||
- `traefik.frontend.priority=10`: override default frontend priority
|
||||
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
|
||||
|
||||
If `AccessKeyID`/`SecretAccessKey` is not given credentials will be resolved in the following order:
|
||||
|
||||
- From environment variables; `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_SESSION_TOKEN`.
|
||||
- Shared credentials, determined by `AWS_PROFILE` and `AWS_SHARED_CREDENTIALS_FILE`, defaults to `default` and `~/.aws/credentials`.
|
||||
- EC2 instance role or ECS task role
|
||||
|
||||
Træfɪk needs the following policy to read ECS information:
|
||||
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "Traefik ECS read access",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ecs:ListTasks",
|
||||
"ecs:DescribeTasks",
|
||||
"ecs:DescribeContainerInstances",
|
||||
"ecs:DescribeTaskDefinition",
|
||||
"ec2:DescribeInstances"
|
||||
],
|
||||
"Resource": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
# Rancher backend
|
||||
|
||||
Træfɪk can be configured to use Rancher as a backend configuration:
|
||||
|
||||
|
||||
```toml
|
||||
################################################################
|
||||
# Rancher configuration backend
|
||||
################################################################
|
||||
|
||||
# Enable Rancher configuration backend
|
||||
#
|
||||
# Optional
|
||||
#
|
||||
[rancher]
|
||||
|
||||
# Default domain used.
|
||||
# Can be overridden by setting the "traefik.domain" label on an service.
|
||||
#
|
||||
# Required
|
||||
#
|
||||
domain = "rancher.localhost"
|
||||
|
||||
# Enable watch Rancher changes
|
||||
#
|
||||
# Optional
|
||||
# Default: true
|
||||
#
|
||||
Watch = true
|
||||
|
||||
# Expose Rancher services by default in traefik
|
||||
#
|
||||
# Optional
|
||||
# Default: true
|
||||
#
|
||||
ExposedByDefault = false
|
||||
|
||||
# Endpoint to use when connecting to Rancher
|
||||
#
|
||||
# Optional
|
||||
# Endpoint = "http://rancherserver.example.com"
|
||||
|
||||
# AccessKey to use when connecting to Rancher
|
||||
#
|
||||
# Optional
|
||||
# AccessKey = "XXXXXXXXX"
|
||||
|
||||
# SecretKey to use when connecting to Rancher
|
||||
#
|
||||
# Optional
|
||||
# SecretKey = "XXXXXXXXXXX"
|
||||
|
||||
```
|
||||
|
||||
If you're deploying traefik as a service within rancher, you can alternatively set these labels on the service to let it only fetch data of its current environment. The settings `endpoint`, `accesskey` and `secretkey` can be omitted then.
|
||||
|
||||
- `io.rancher.container.create_agent=true`
|
||||
- `io.rancher.container.agent.role=environment`
|
||||
|
||||
Labels can be used on task containers to override default behaviour:
|
||||
|
||||
- `traefik.protocol=https`: override the default `http` protocol
|
||||
- `traefik.weight=10`: assign this weight to the container
|
||||
- `traefik.enable=false`: disable this container in Træfɪk
|
||||
- `traefik.frontend.rule=Host:test.traefik.io`: override the default frontend rule (Default: `Host:{containerName}.{domain}`).
|
||||
- `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend.
|
||||
- `traefik.frontend.priority=10`: override default frontend priority
|
||||
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
|
||||
|
@@ -114,4 +114,20 @@ defaultEntryPoints = ["http"]
|
||||
address = ":80"
|
||||
[entryPoints.http.auth.basic]
|
||||
users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
|
||||
```
|
||||
|
||||
## Pass Authenticated user to application via headers
|
||||
|
||||
Providing an authentication method as described above, it is possible to pass the user to the application
|
||||
via a configurable header value
|
||||
|
||||
```
|
||||
defaultEntryPoints = ["http"]
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":80"
|
||||
[entryPoints.http.auth]
|
||||
headerField = "X-WebAuth-User"
|
||||
[entryPoints.http.auth.basic]
|
||||
users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"]
|
||||
```
|
@@ -1,6 +1,6 @@
|
||||
# Kubernetes Ingress Controller
|
||||
|
||||
This guide explains how to use Træfɪk as an Ingress controller in a Kubernetes cluster.
|
||||
This guide explains how to use Træfɪk as an Ingress controller in a Kubernetes cluster.
|
||||
If you are not familiar with Ingresses in Kubernetes you might want to read the [Kubernetes user guide](http://kubernetes.io/docs/user-guide/ingress/)
|
||||
|
||||
The config files used in this guide can be found in the [examples directory](https://github.com/containous/traefik/tree/master/examples/k8s)
|
||||
@@ -19,7 +19,6 @@ We are going to deploy Træfɪk with a
|
||||
allow you to easily roll out config changes or update the image.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Deployment
|
||||
apiVersion: extensions/v1beta1
|
||||
metadata:
|
||||
@@ -37,11 +36,10 @@ spec:
|
||||
labels:
|
||||
k8s-app: traefik-ingress-lb
|
||||
name: traefik-ingress-lb
|
||||
version: v1.0.0
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 60
|
||||
containers:
|
||||
- image: traefik:v1.0.0
|
||||
- image: traefik
|
||||
name: traefik-ingress-lb
|
||||
resources:
|
||||
limits:
|
||||
@@ -86,7 +84,7 @@ traefik-ingress-controller-678226159-eqseo 1/1 Running 0 7m
|
||||
```
|
||||
|
||||
You should see that after submitting the Deployment to Kubernetes it has launched
|
||||
a pod, and it is now running. _It might take a few moments for kubenetes to pull
|
||||
a pod, and it is now running. _It might take a few moments for kubernetes to pull
|
||||
the Træfɪk image and start the container._
|
||||
|
||||
> You could also check the deployment with the Kubernetes dashboard, run
|
||||
@@ -115,7 +113,7 @@ metadata:
|
||||
namespace: kube-system
|
||||
spec:
|
||||
selector:
|
||||
k8s-app: traefik-ingress-lb
|
||||
k8s-app: traefik-ingress-lb
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
@@ -141,7 +139,7 @@ kubectl apply -f examples/k8s/ui.yaml
|
||||
```
|
||||
|
||||
Now lets setup an entry in our /etc/hosts file to route `traefik-ui.local`
|
||||
to our cluster.
|
||||
to our cluster.
|
||||
|
||||
> In production you would want to set up real dns entries.
|
||||
|
||||
@@ -301,6 +299,8 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: wensleydale
|
||||
annotations:
|
||||
traefik.backend.circuitbreaker: "NetworkErrorRatio() > 0.5"
|
||||
spec:
|
||||
ports:
|
||||
- name: http
|
||||
@@ -310,6 +310,11 @@ spec:
|
||||
app: cheese
|
||||
task: wensleydale
|
||||
```
|
||||
|
||||
> Notice that we also set a [circuit breaker expression](https://docs.traefik.io/basics/#backends) for one of the backends
|
||||
> by setting the `traefik.backend.circuitbreaker` annotation on the service.
|
||||
|
||||
|
||||
[examples/k8s/cheese-services.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/cheese-services.yaml)
|
||||
|
||||
```sh
|
||||
|
@@ -4,7 +4,7 @@ This section explains how to create a multi-host docker cluster with
|
||||
swarm mode using [docker-machine](https://docs.docker.com/machine) and
|
||||
how to deploy Træfɪk on it.
|
||||
|
||||
The cluster constist of:
|
||||
The cluster consists of:
|
||||
|
||||
- 3 servers
|
||||
- 1 manager
|
||||
@@ -19,7 +19,7 @@ The cluster constist of:
|
||||
|
||||
## Cluster provisioning
|
||||
|
||||
First, let's create all the nodes required. It's a shorter version of
|
||||
First, let's create all the required nodes. It's a shorter version of
|
||||
the [swarm tutorial](https://docs.docker.com/engine/swarm/swarm-tutorial/).
|
||||
|
||||
```sh
|
||||
@@ -47,6 +47,7 @@ docker-machine ssh worker1 "docker swarm join \
|
||||
--listen-addr $(docker-machine ip worker1) \
|
||||
--advertise-addr $(docker-machine ip worker1) \
|
||||
$(docker-machine ip manager)"
|
||||
|
||||
docker-machine ssh worker2 "docker swarm join \
|
||||
--token=${worker_token} \
|
||||
--listen-addr $(docker-machine ip worker2) \
|
||||
@@ -103,7 +104,7 @@ Let's explain this command:
|
||||
we bind mount the docker socket where Træfik is scheduled to be able
|
||||
to speak to the daemon.
|
||||
- `--network traefik-net`: we attach the Træfik service (and thus
|
||||
the underlined container) to the `traefik-net` network.
|
||||
the underlying container) to the `traefik-net` network.
|
||||
- `--docker`: enable docker backend, and `--docker.swarmmode` to
|
||||
enable the swarm mode on Træfik.
|
||||
- `--web`: activate the webUI on port 8080
|
||||
@@ -120,13 +121,18 @@ docker-machine ssh manager "docker service create \
|
||||
--label traefik.port=80 \
|
||||
--network traefik-net \
|
||||
emilevauge/whoami"
|
||||
|
||||
docker-machine ssh manager "docker service create \
|
||||
--name whoami1 \
|
||||
--label traefik.port=80 \
|
||||
--network traefik-net \
|
||||
--label traefik.backend.loadbalancer.sticky=true \
|
||||
emilevauge/whoami"
|
||||
```
|
||||
|
||||
Note that we set whoami1 to use sticky sessions (`--label traefik.backend.loadbalancer.sticky=true`). We'll demonstrate that later.
|
||||
If using `docker stack deploy`, there is [a specific way that the labels must be defined in the docker-compose file](https://github.com/containous/traefik/issues/994#issuecomment-269095109).
|
||||
|
||||
Check that everything is scheduled and started:
|
||||
|
||||
```sh
|
||||
@@ -218,6 +224,84 @@ X-Forwarded-Proto: http
|
||||
X-Forwarded-Server: 8fbc39271b4c
|
||||
```
|
||||
|
||||
## Scale both services
|
||||
|
||||
```sh
|
||||
docker-machine ssh manager "docker service scale whoami0=5"
|
||||
|
||||
docker-machine ssh manager "docker service scale whoami1=5"
|
||||
```
|
||||
|
||||
|
||||
Check that we now have 5 replicas of each `whoami` service:
|
||||
|
||||
```sh
|
||||
docker-machine ssh manager "docker service ls"
|
||||
ID NAME REPLICAS IMAGE COMMAND
|
||||
ab046gpaqtln whoami0 5/5 emilevauge/whoami
|
||||
cgfg5ifzrpgm whoami1 5/5 emilevauge/whoami
|
||||
dtpl249tfghc traefik 1/1 traefik --docker --docker.swarmmode --docker.domain=traefik --docker.watch --web
|
||||
```
|
||||
## Access to your whoami0 through Træfɪk multiple times.
|
||||
|
||||
Repeat the following command multiple times and note that the Hostname changes each time as Traefik load balances each request against the 5 tasks.
|
||||
```sh
|
||||
curl -H Host:whoami0.traefik http://$(docker-machine ip manager)
|
||||
Hostname: 8147a7746e7a
|
||||
IP: 127.0.0.1
|
||||
IP: ::1
|
||||
IP: 10.0.9.3
|
||||
IP: fe80::42:aff:fe00:903
|
||||
IP: 172.18.0.3
|
||||
IP: fe80::42:acff:fe12:3
|
||||
GET / HTTP/1.1
|
||||
Host: 10.0.9.3:80
|
||||
User-Agent: curl/7.35.0
|
||||
Accept: */*
|
||||
Accept-Encoding: gzip
|
||||
X-Forwarded-For: 192.168.99.1
|
||||
X-Forwarded-Host: 10.0.9.3:80
|
||||
X-Forwarded-Proto: http
|
||||
X-Forwarded-Server: 8fbc39271b4c
|
||||
```
|
||||
|
||||
Do the same against whoami1.
|
||||
```sh
|
||||
curl -H Host:whoami1.traefik http://$(docker-machine ip manager)
|
||||
Hostname: ba2c21488299
|
||||
IP: 127.0.0.1
|
||||
IP: ::1
|
||||
IP: 10.0.9.4
|
||||
IP: fe80::42:aff:fe00:904
|
||||
IP: 172.18.0.2
|
||||
IP: fe80::42:acff:fe12:2
|
||||
GET / HTTP/1.1
|
||||
Host: 10.0.9.4:80
|
||||
User-Agent: curl/7.35.0
|
||||
Accept: */*
|
||||
Accept-Encoding: gzip
|
||||
X-Forwarded-For: 192.168.99.1
|
||||
X-Forwarded-Host: 10.0.9.4:80
|
||||
X-Forwarded-Proto: http
|
||||
X-Forwarded-Server: 8fbc39271b4c
|
||||
```
|
||||
Wait, I thought we added the sticky flag to whoami1? Traefik relies on a cookie to maintain stickyness so you'll need to test this with a browser.
|
||||
|
||||
First you need to add whoami1.traefik to your hosts file:
|
||||
```ssh
|
||||
if [ -n "$(grep whoami1.traefik /etc/hosts)" ];
|
||||
then
|
||||
echo "whoami1.traefik already exists (make sure the ip is current)";
|
||||
else
|
||||
sudo -- sh -c -e "echo '$(docker-machine ip manager)\twhoami1.traefik'
|
||||
>> /etc/hosts";
|
||||
fi
|
||||
```
|
||||
|
||||
Now open your browser and go to http://whoami1.traefik/
|
||||
|
||||
You will now see that stickyness is maintained.
|
||||
|
||||

|
||||
|
||||
|
||||
|
@@ -1,12 +1,11 @@
|
||||
kubelet:
|
||||
image: gcr.io/google_containers/hyperkube-amd64:v1.2.2
|
||||
image: gcr.io/google_containers/hyperkube-amd64:v1.5.2
|
||||
privileged: true
|
||||
pid: host
|
||||
net : host
|
||||
volumes:
|
||||
- /:/rootfs:ro
|
||||
- /sys:/sys:ro
|
||||
- /sys:/sys:rw
|
||||
- /var/lib/docker/:/var/lib/docker:rw
|
||||
- /var/lib/kubelet/:/var/lib/kubelet:rw
|
||||
- /var/lib/kubelet/:/var/lib/kubelet:rw,shared
|
||||
- /var/run:/var/run:rw
|
||||
command: ['/hyperkube', 'kubelet', '--containerized', '--hostname-override=127.0.0.1', '--address=0.0.0.0', '--api-servers=http://localhost:8080', '--config=/etc/kubernetes/manifests', '--allow-privileged=true', '--v=2']
|
||||
command: ['/hyperkube', 'kubelet', '--hostname-override=127.0.0.1', '--api-servers=http://localhost:8080', '--config=/etc/kubernetes/manifests', '--allow-privileged=true', '--v=2', '--cluster-dns=10.0.0.10', '--cluster-domain=cluster.local']
|
||||
|
10
examples/compose-rancher.yml
Normal file
10
examples/compose-rancher.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
traefik:
|
||||
image: traefik
|
||||
command: --web --rancher --rancher.domain=rancher.localhost --logLevel=DEBUG
|
||||
labels:
|
||||
io.rancher.container.agent.role: environment
|
||||
io.rancher.container.create_agent: 'true'
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8080:8080"
|
@@ -16,12 +16,11 @@ spec:
|
||||
labels:
|
||||
k8s-app: traefik-ingress-lb
|
||||
name: traefik-ingress-lb
|
||||
version: v1.1.0
|
||||
spec:
|
||||
terminationGracePeriodSeconds: 60
|
||||
hostNetwork: true
|
||||
containers:
|
||||
- image: traefik:v1.1.0
|
||||
- image: traefik
|
||||
name: traefik-ingress-lb
|
||||
resources:
|
||||
limits:
|
||||
@@ -35,7 +34,9 @@ spec:
|
||||
containerPort: 80
|
||||
hostPort: 80
|
||||
- name: admin
|
||||
containerPort: 8080
|
||||
containerPort: 8081
|
||||
args:
|
||||
- -d
|
||||
- --web
|
||||
- --web.address=:8081
|
||||
- --kubernetes
|
||||
|
@@ -10,7 +10,7 @@ spec:
|
||||
ports:
|
||||
- name: web
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
targetPort: 8081
|
||||
---
|
||||
apiVersion: extensions/v1beta1
|
||||
kind: Ingress
|
||||
|
560
glide.lock
generated
560
glide.lock
generated
@@ -1,10 +1,73 @@
|
||||
hash: 1bbeb842ee639ccc6e2edf8cc13fc2759cb96e3d839a1aec7b7f6af4fb89c8e1
|
||||
updated: 2016-11-09T19:24:00.762904389+01:00
|
||||
hash: b689cb0faed68086641d9e3504ee29498e5bf06b088ad4fcd1e76543446d4d9a
|
||||
updated: 2017-03-27T14:29:54.009570184+02:00
|
||||
imports:
|
||||
- name: bitbucket.org/ww/goautoneg
|
||||
version: 75cd24fc2f2c2a2088577d12123ddee5f54e0675
|
||||
- name: cloud.google.com/go
|
||||
version: c116c7972ec94f148459a304d07a67ecbc770d4b
|
||||
subpackages:
|
||||
- compute/metadata
|
||||
- name: github.com/abbot/go-http-auth
|
||||
version: cb4372376e1e00e9f6ab9ec142e029302c9e7140
|
||||
- name: github.com/ArthurHlt/go-eureka-client
|
||||
version: ba361cd0f9f571b4e871421423d2f02f5689c3d2
|
||||
subpackages:
|
||||
- eureka
|
||||
- name: github.com/ArthurHlt/gominlog
|
||||
version: 068c01ce147ad68fca25ef3fa29ae5395ae273ab
|
||||
- name: github.com/aws/aws-sdk-go
|
||||
version: 3f8f870ec9939e32b3372abf74d24e468bcd285d
|
||||
subpackages:
|
||||
- aws
|
||||
- aws/awserr
|
||||
- aws/awsutil
|
||||
- aws/client
|
||||
- aws/client/metadata
|
||||
- aws/corehandlers
|
||||
- aws/credentials
|
||||
- aws/credentials/ec2rolecreds
|
||||
- aws/credentials/endpointcreds
|
||||
- aws/credentials/stscreds
|
||||
- aws/defaults
|
||||
- aws/ec2metadata
|
||||
- aws/endpoints
|
||||
- aws/request
|
||||
- aws/session
|
||||
- aws/signer/v4
|
||||
- private/protocol
|
||||
- private/protocol/ec2query
|
||||
- private/protocol/json/jsonutil
|
||||
- private/protocol/jsonrpc
|
||||
- private/protocol/query
|
||||
- private/protocol/query/queryutil
|
||||
- private/protocol/rest
|
||||
- private/protocol/restxml
|
||||
- private/protocol/xml/xmlutil
|
||||
- private/waiter
|
||||
- service/ec2
|
||||
- service/ecs
|
||||
- service/route53
|
||||
- service/sts
|
||||
- name: github.com/Azure/azure-sdk-for-go
|
||||
version: 1620af6b32398bfc91827ceae54a8cc1f55df04d
|
||||
subpackages:
|
||||
- arm/dns
|
||||
- name: github.com/Azure/go-autorest
|
||||
version: 32cc2321122a649b7ba4e323527bcb145134fd47
|
||||
subpackages:
|
||||
- autorest
|
||||
- autorest/azure
|
||||
- autorest/date
|
||||
- autorest/to
|
||||
- autorest/validation
|
||||
- name: github.com/beorn7/perks
|
||||
version: b965b613227fddccbfffe13eae360ed3fa822f8d
|
||||
subpackages:
|
||||
- quantile
|
||||
- name: github.com/blang/semver
|
||||
version: 3a37c301dda64cbe17f16f661b4c976803c0e2d2
|
||||
- name: github.com/boltdb/bolt
|
||||
version: f4c032d907f61f08dba2d719c58f108a1abb8e81
|
||||
version: 5cc10bbbc5c141029940133bb33c9e969512a698
|
||||
- name: github.com/BurntSushi/toml
|
||||
version: 99064174e013895bbd9b025c31100bd1d9b590ca
|
||||
- name: github.com/BurntSushi/ty
|
||||
@@ -14,11 +77,11 @@ imports:
|
||||
- name: github.com/cenk/backoff
|
||||
version: 8edc80b07f38c27352fb186d971c628a6c32552b
|
||||
- name: github.com/codahale/hdrhistogram
|
||||
version: f8ad88b59a584afeee9d334eff879b104439117b
|
||||
version: 9208b142303c12d8899bae836fd524ac9338b4fd
|
||||
- name: github.com/codegangsta/cli
|
||||
version: 1efa31f08b9333f1bd4882d61f9d668a70cd902e
|
||||
version: bf4a526f48af7badd25d2cb02d587e1b01be3b50
|
||||
- name: github.com/codegangsta/negroni
|
||||
version: 3f7ce7b928e14ff890b067e5bbbc80af73690a9c
|
||||
version: dc6b9d037e8dab60cbfc09c61d6932537829be8b
|
||||
- name: github.com/containous/flaeg
|
||||
version: a731c034dda967333efce5f8d276aeff11f8ff87
|
||||
- name: github.com/containous/mux
|
||||
@@ -26,21 +89,48 @@ imports:
|
||||
- name: github.com/containous/staert
|
||||
version: 1e26a71803e428fd933f5f9c8e50a26878f53147
|
||||
- name: github.com/coreos/etcd
|
||||
version: 1c9e0a0e33051fed6c05c141e6fcbfe5c7f2a899
|
||||
version: c400d05d0aa73e21e431c16145e558d624098018
|
||||
subpackages:
|
||||
- Godeps/_workspace/src/github.com/ugorji/go/codec
|
||||
- Godeps/_workspace/src/golang.org/x/net/context
|
||||
- client
|
||||
- pkg/pathutil
|
||||
- pkg/types
|
||||
- name: github.com/coreos/go-oidc
|
||||
version: 9e117111587506b9dc83b7b38263268bf48352ea
|
||||
subpackages:
|
||||
- http
|
||||
- jose
|
||||
- key
|
||||
- oauth2
|
||||
- oidc
|
||||
- name: github.com/coreos/go-systemd
|
||||
version: 43e4800a6165b4e02bb2a36673c54b230d6f7b26
|
||||
version: 48702e0da86bd25e76cfef347e2adeb434a0d0a6
|
||||
subpackages:
|
||||
- daemon
|
||||
- name: github.com/coreos/pkg
|
||||
version: 2c77715c4df99b5420ffcae14ead08f52104065d
|
||||
subpackages:
|
||||
- capnslog
|
||||
- health
|
||||
- httputil
|
||||
- timeutil
|
||||
- name: github.com/davecgh/go-spew
|
||||
version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9
|
||||
subpackages:
|
||||
- spew
|
||||
- name: github.com/daviddengcn/go-colortext
|
||||
version: 3b18c8575a432453d41fdafb340099fff5bba2f7
|
||||
- name: github.com/decker502/dnspod-go
|
||||
version: f6b1d56f1c048bd94d7e42ac36efb4d57b069b6f
|
||||
- name: github.com/dgrijalva/jwt-go
|
||||
version: 9ed569b5d1ac936e6494082958d63a6aa4fff99a
|
||||
- name: github.com/dnsimple/dnsimple-go
|
||||
version: eeb343928d9a3de357a650c8c25d8f1318330d57
|
||||
subpackages:
|
||||
- dnsimple
|
||||
- name: github.com/docker/distribution
|
||||
version: 99cb7c0946d2f5a38015443e515dc916295064d7
|
||||
version: 325b0804fef3a66309d962357aac3c2ce3f4d329
|
||||
subpackages:
|
||||
- context
|
||||
- digest
|
||||
@@ -49,14 +139,25 @@ imports:
|
||||
- registry/api/v2
|
||||
- registry/client
|
||||
- registry/client/auth
|
||||
- registry/client/auth/challenge
|
||||
- registry/client/transport
|
||||
- registry/storage/cache
|
||||
- registry/storage/cache/memory
|
||||
- uuid
|
||||
- name: github.com/docker/docker
|
||||
version: 534753663161334baba06f13b8efa4cad22b5bc5
|
||||
version: 49bf474f9ed7ce7143a59d1964ff7b7fd9b52178
|
||||
subpackages:
|
||||
- api/types
|
||||
- api/types/backend
|
||||
- api/types/blkiodev
|
||||
- api/types/container
|
||||
- api/types/filters
|
||||
- api/types/mount
|
||||
- api/types/network
|
||||
- api/types/registry
|
||||
- api/types/strslice
|
||||
- api/types/swarm
|
||||
- api/types/versions
|
||||
- builder
|
||||
- builder/dockerignore
|
||||
- cliconfig
|
||||
@@ -66,6 +167,7 @@ imports:
|
||||
- image/v1
|
||||
- layer
|
||||
- namesgenerator
|
||||
- oci
|
||||
- opts
|
||||
- pkg/archive
|
||||
- pkg/chrootarchive
|
||||
@@ -78,9 +180,9 @@ imports:
|
||||
- pkg/jsonlog
|
||||
- pkg/jsonmessage
|
||||
- pkg/longpath
|
||||
- pkg/mflag
|
||||
- pkg/mount
|
||||
- pkg/namesgenerator
|
||||
- pkg/plugingetter
|
||||
- pkg/plugins
|
||||
- pkg/plugins/transport
|
||||
- pkg/pools
|
||||
@@ -98,11 +200,12 @@ imports:
|
||||
- pkg/term
|
||||
- pkg/term/windows
|
||||
- pkg/urlutil
|
||||
- plugin/v2
|
||||
- reference
|
||||
- registry
|
||||
- runconfig/opts
|
||||
- name: github.com/docker/engine-api
|
||||
version: 62043eb79d581a32ea849645277023c550732e52
|
||||
version: 3d1601b9d2436a70b0dfc045a23f6503d19195df
|
||||
subpackages:
|
||||
- client
|
||||
- client/transport
|
||||
@@ -120,34 +223,17 @@ imports:
|
||||
- types/time
|
||||
- types/versions
|
||||
- name: github.com/docker/go-connections
|
||||
version: 988efe982fdecb46f01d53465878ff1f2ff411ce
|
||||
version: 990a1a1a70b0da4c4cb70e117971a4f0babfbf1a
|
||||
subpackages:
|
||||
- nat
|
||||
- sockets
|
||||
- tlsconfig
|
||||
- name: github.com/docker/go-units
|
||||
version: f2145db703495b2e525c59662db69a7344b00bb8
|
||||
version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
|
||||
- name: github.com/docker/leadership
|
||||
version: 0a913e2d71a12fd14a028452435cb71ac8d82cb6
|
||||
- name: github.com/docker/libcompose
|
||||
version: d1876c1d68527a49c0aac22a0b161acc7296b740
|
||||
subpackages:
|
||||
- config
|
||||
- docker
|
||||
- docker/builder
|
||||
- docker/client
|
||||
- docker/network
|
||||
- labels
|
||||
- logger
|
||||
- lookup
|
||||
- project
|
||||
- project/events
|
||||
- project/options
|
||||
- utils
|
||||
- version
|
||||
- yaml
|
||||
version: bfc7753dd48af19513b29deec23c364bf0f274eb
|
||||
- name: github.com/docker/libkv
|
||||
version: 3fce6a0f26e07da3eac45796a8e255547a47a750
|
||||
version: 35d3e2084c650109e7bcc7282655b1bc8ba924ff
|
||||
subpackages:
|
||||
- store
|
||||
- store/boltdb
|
||||
@@ -156,54 +242,109 @@ imports:
|
||||
- store/zookeeper
|
||||
- name: github.com/donovanhide/eventsource
|
||||
version: fd1de70867126402be23c306e1ce32828455d85b
|
||||
- name: github.com/eapache/channels
|
||||
version: 47238d5aae8c0fefd518ef2bee46290909cf8263
|
||||
- name: github.com/eapache/queue
|
||||
version: 44cc805cf13205b55f69e14bcb69867d1ae92f98
|
||||
- name: github.com/edeckers/auroradnsclient
|
||||
version: 8b777c170cfd377aa16bb4368f093017dddef3f9
|
||||
subpackages:
|
||||
- records
|
||||
- requests
|
||||
- requests/errors
|
||||
- tokens
|
||||
- zones
|
||||
- name: github.com/elazarl/go-bindata-assetfs
|
||||
version: 9a6736ed45b44bf3835afeebb3034b57ed329f3e
|
||||
version: 57eb5e1fc594ad4b0b1dbea7b286d299e0cb43c2
|
||||
- name: github.com/emicklei/go-restful
|
||||
version: 892402ba11a2e2fd5e1295dd633481f27365f14d
|
||||
subpackages:
|
||||
- log
|
||||
- swagger
|
||||
- name: github.com/gambol99/go-marathon
|
||||
version: a558128c87724cd7430060ef5aedf39f83937f55
|
||||
- name: github.com/go-check/check
|
||||
version: 4f90aeace3a26ad7021961c297b22c42160c7b25
|
||||
version: 6b00a5b651b1beb2c6821863f7c60df490bd46c8
|
||||
- name: github.com/ghodss/yaml
|
||||
version: 04f313413ffd65ce25f2541bfd2b2ceec5c0908c
|
||||
- name: github.com/go-ini/ini
|
||||
version: 6f66b0e091edb3c7b380f7c4f0f884274d550b67
|
||||
- name: github.com/go-kit/kit
|
||||
version: f66b0e13579bfc5a48b9e2a94b1209c107ea1f41
|
||||
subpackages:
|
||||
- metrics
|
||||
- metrics/internal/lv
|
||||
- metrics/prometheus
|
||||
- name: github.com/go-openapi/jsonpointer
|
||||
version: 8d96a2dc61536b690bd36b2e9df0b3c0b62825b2
|
||||
- name: github.com/go-openapi/jsonreference
|
||||
version: 36d33bfe519efae5632669801b180bf1a245da3b
|
||||
- name: github.com/go-openapi/spec
|
||||
version: 34b5ffff717ab4535aef76e3dd90818bddde571b
|
||||
- name: github.com/go-openapi/swag
|
||||
version: 96d7b9ebd181a1735a1c9ac87914f2b32fbf56c9
|
||||
- name: github.com/gogo/protobuf
|
||||
version: 99cb9b23110011cc45571c901ecae6f6f5e65cd3
|
||||
version: 909568be09de550ed094403c2bf8a261b5bb730a
|
||||
subpackages:
|
||||
- proto
|
||||
- sortkeys
|
||||
- name: github.com/golang/glog
|
||||
version: fca8c8854093a154ff1eb580aae10276ad6b1b5f
|
||||
- name: github.com/golang/protobuf
|
||||
version: 5677a0e3d5e89854c9974e1256839ee23f8233ca
|
||||
subpackages:
|
||||
- proto
|
||||
- name: github.com/google/go-github
|
||||
version: 55263f30529cb06f5b478efc333390b791cfe3b1
|
||||
version: c8ebe3a4d7f0791a6315b7410353d4084c58805d
|
||||
subpackages:
|
||||
- github
|
||||
- name: github.com/google/go-querystring
|
||||
version: 9235644dd9e52eeae6fa48efd539fdc351a0af53
|
||||
subpackages:
|
||||
- query
|
||||
- name: github.com/google/gofuzz
|
||||
version: 44d81051d367757e1c7c6a5a86423ece9afcf63c
|
||||
- name: github.com/gorilla/context
|
||||
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
|
||||
version: 1ea25387ff6f684839d82767c1733ff4d4d15d0a
|
||||
- name: github.com/gorilla/websocket
|
||||
version: 4873052237e4eeda85cf50c071ef33836fe8e139
|
||||
- name: github.com/hashicorp/consul
|
||||
version: d8e2fb7dd594163e25a89bc52c1a4613f5c5bfb8
|
||||
version: fce7d75609a04eeb9d4bf41c8dc592aac18fc97d
|
||||
subpackages:
|
||||
- api
|
||||
- name: github.com/hashicorp/go-cleanhttp
|
||||
version: ad28ea4487f05916463e2423a55166280e8254b5
|
||||
version: 875fb671b3ddc66f8e2f0acc33829c8cb989a38d
|
||||
- name: github.com/hashicorp/go-version
|
||||
version: e96d3840402619007766590ecea8dd7af1292276
|
||||
- name: github.com/hashicorp/serf
|
||||
version: b03bf85930b2349eb04b97c8fac437495296e3e7
|
||||
version: 6c4672d66fc6312ddde18399262943e21175d831
|
||||
subpackages:
|
||||
- coordinate
|
||||
- name: github.com/jarcoal/httpmock
|
||||
version: 145b10d659265440f062c31ea15326166bae56ee
|
||||
- name: github.com/libkermit/compose
|
||||
version: cadc5a3b83a15790174bd7fbc75ea2529785e772
|
||||
- serf
|
||||
- name: github.com/JamesClonk/vultr
|
||||
version: 9ec0427d51411407c0402b093a1771cb75af9679
|
||||
subpackages:
|
||||
- check
|
||||
- name: github.com/libkermit/docker
|
||||
version: 55e3595409924fcfbb850811e5a7cdbe8960a0b7
|
||||
- lib
|
||||
- name: github.com/jmespath/go-jmespath
|
||||
version: bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d
|
||||
- name: github.com/jonboulle/clockwork
|
||||
version: 72f9bd7c4e0c2a40055ab3d0f09654f730cce982
|
||||
- name: github.com/juju/ratelimit
|
||||
version: 77ed1c8a01217656d2080ad51981f6e99adaa177
|
||||
- name: github.com/mailgun/manners
|
||||
version: a585afd9d65c0e05f6c003f921e71ebc05074f4f
|
||||
- name: github.com/mailgun/timetools
|
||||
version: fd192d755b00c968d312d23f521eb0cdc6f66bd0
|
||||
- name: github.com/mailru/easyjson
|
||||
version: 9d6630dc8c577b56cb9687a9cf9e8578aca7298a
|
||||
subpackages:
|
||||
- buffer
|
||||
- jlexer
|
||||
- jwriter
|
||||
- name: github.com/mattn/go-shellwords
|
||||
version: 525bedee691b5a8df547cb5cf9f86b7fb1883e24
|
||||
- name: github.com/matttproud/golang_protobuf_extensions
|
||||
version: fc2b8d3a73c4867e51861bbdd5ae3c1f0869dd6a
|
||||
subpackages:
|
||||
- pbutil
|
||||
- name: github.com/mesos/mesos-go
|
||||
version: 068d5470506e3780189fe607af40892814197c5e
|
||||
subpackages:
|
||||
@@ -229,62 +370,101 @@ imports:
|
||||
- name: github.com/Microsoft/go-winio
|
||||
version: ce2922f643c8fd76b46cadc7f404a06282678b34
|
||||
- name: github.com/miekg/dns
|
||||
version: 5d001d020961ae1c184f9f8152fdc73810481677
|
||||
version: 8060d9f51305bbe024b99679454e62f552cd0b0b
|
||||
- name: github.com/mitchellh/mapstructure
|
||||
version: f3009df150dadf309fdee4a54ed65c124afad715
|
||||
- name: github.com/moul/http2curl
|
||||
version: b1479103caacaa39319f75e7f57fc545287fca0d
|
||||
- name: github.com/mvdan/xurls
|
||||
version: fa08908f19eca8c491d68c6bd8b4b44faea6daf8
|
||||
- name: github.com/NYTimes/gziphandler
|
||||
version: f6438dbf4a82c56684964b03956aa727b0d7816b
|
||||
version: 6710af535839f57c687b62c4c23d649f9545d885
|
||||
- name: github.com/ogier/pflag
|
||||
version: 45c278ab3607870051a2ea9040bb85fcb8557481
|
||||
- name: github.com/opencontainers/runc
|
||||
version: 02f8fa7863dd3f82909a73e2061897828460d52f
|
||||
version: 1a81e9ab1f138c091fe5c86d0883f87716088527
|
||||
subpackages:
|
||||
- libcontainer/configs
|
||||
- libcontainer/devices
|
||||
- libcontainer/system
|
||||
- libcontainer/user
|
||||
- name: github.com/parnurzeal/gorequest
|
||||
version: e30af16d4e485943aab0b0885ad6bdbb8c0d3dc7
|
||||
- name: github.com/ovh/go-ovh
|
||||
version: a8a4c0bc40e56322142649bda7b2b4bb15145b6e
|
||||
subpackages:
|
||||
- ovh
|
||||
- name: github.com/pborman/uuid
|
||||
version: 5007efa264d92316c43112bc573e754bc889b7b1
|
||||
- name: github.com/pkg/errors
|
||||
version: bfd5150e4e41705ded2129ec33379de1cb90b513
|
||||
- name: github.com/pmezard/go-difflib
|
||||
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
|
||||
subpackages:
|
||||
- difflib
|
||||
- name: github.com/prometheus/client_golang
|
||||
version: c5b7fccd204277076155f10851dad72b76a49317
|
||||
subpackages:
|
||||
- prometheus
|
||||
- prometheus/promhttp
|
||||
- name: github.com/prometheus/client_model
|
||||
version: fa8ad6fec33561be4280a8f0514318c79d7f6cb6
|
||||
subpackages:
|
||||
- go
|
||||
- name: github.com/prometheus/common
|
||||
version: ffe929a3f4c4faeaa10f2b9535c2b1be3ad15650
|
||||
subpackages:
|
||||
- expfmt
|
||||
- model
|
||||
- name: github.com/prometheus/procfs
|
||||
version: 454a56f35412459b5e684fd5ec0f9211b94f002a
|
||||
- name: github.com/PuerkitoBio/purell
|
||||
version: 0bcb03f4b4d0a9428594752bd2a3b9aa0a9d4bd4
|
||||
- name: github.com/PuerkitoBio/urlesc
|
||||
version: 5bd2802263f21d8788851d5305584c82a5c75d7e
|
||||
- name: github.com/pyr/egoscale
|
||||
version: ab4b0d7ff424c462da486aef27f354cdeb29a319
|
||||
subpackages:
|
||||
- src/egoscale
|
||||
- name: github.com/rancher/go-rancher
|
||||
version: 5b8f6cc26b355ba03d7611fce3844155b7baf05b
|
||||
subpackages:
|
||||
- client
|
||||
- name: github.com/ryanuber/go-glob
|
||||
version: 572520ed46dbddaed19ea3d9541bdd0494163693
|
||||
- name: github.com/samuel/go-zookeeper
|
||||
version: 87e1bca4477a3cc767ca71be023ced183d74e538
|
||||
version: e64db453f3512cade908163702045e0f31137843
|
||||
subpackages:
|
||||
- zk
|
||||
- name: github.com/satori/go.uuid
|
||||
version: 879c5887cd475cd7864858769793b2ceb0d44feb
|
||||
- name: github.com/Sirupsen/logrus
|
||||
version: 3ec0642a7fb6488f65b06f9040adc67e3990296a
|
||||
version: a283a10442df8dc09befd873fab202bf8a253d6a
|
||||
- name: github.com/spf13/pflag
|
||||
version: 5644820622454e71517561946e3d94b9f9db6842
|
||||
- name: github.com/streamrail/concurrent-map
|
||||
version: 8bf1e9bacbf65b10c81d0f4314cf2b1ebef728b5
|
||||
version: 65a174a3a4188c0b7099acbc6cfa0c53628d3287
|
||||
- name: github.com/stretchr/objx
|
||||
version: cbeaeb16a013161a98496fad62933b1d21786672
|
||||
- name: github.com/stretchr/testify
|
||||
version: 976c720a22c8eb4eb6a0b4348ad85ad12491a506
|
||||
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
|
||||
subpackages:
|
||||
- assert
|
||||
- mock
|
||||
- name: github.com/thoas/stats
|
||||
version: 152b5d051953fdb6e45f14b6826962aadc032324
|
||||
- name: github.com/timewasted/linode
|
||||
version: 37e84520dcf74488f67654f9c775b9752c232dc1
|
||||
subpackages:
|
||||
- dns
|
||||
- name: github.com/tv42/zbase32
|
||||
version: 03389da7e0bf9844767f82690f4d68fc097a1306
|
||||
- name: github.com/ugorji/go
|
||||
version: b94837a2404ab90efe9289e77a70694c355739cb
|
||||
version: ea9cd21fa0bc41ee4bdd50ac7ed8cbc7ea2ed960
|
||||
subpackages:
|
||||
- codec
|
||||
- name: github.com/unrolled/render
|
||||
version: 526faf80cd4b305bb8134abea8d20d5ced74faa6
|
||||
- name: github.com/urfave/negroni
|
||||
version: e0e50f7dc431c043cb33f91b09c3419d48b7cff5
|
||||
version: 198ad4d8b8a4612176b804ca10555b222a086b40
|
||||
- name: github.com/vdemeester/docker-events
|
||||
version: be74d4929ec1ad118df54349fda4b0cba60f849b
|
||||
- name: github.com/vdemeester/shakers
|
||||
version: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3
|
||||
- name: github.com/vulcand/oxy
|
||||
version: fcc76b52eb8568540a020b7a99e854d9d752b364
|
||||
version: f88530866c561d24a6b5aac49f76d6351b788b9f
|
||||
repo: https://github.com/containous/oxy.git
|
||||
vcs: git
|
||||
subpackages:
|
||||
@@ -300,44 +480,231 @@ imports:
|
||||
- name: github.com/vulcand/route
|
||||
version: cb89d787ddbb1c5849a7ac9f79004c1fd12a4a32
|
||||
- name: github.com/vulcand/vulcand
|
||||
version: bed092e10989250b48bdb6aa3b0557b207f05c80
|
||||
version: 42492a3a85e294bdbdd1bcabb8c12769a81ea284
|
||||
subpackages:
|
||||
- conntracker
|
||||
- plugin
|
||||
- plugin/rewrite
|
||||
- router
|
||||
- name: github.com/xenolf/lego
|
||||
version: b2fad6198110326662e9e356a97199078a4a775c
|
||||
version: 0e2937900b224325f4476745a9b53aef246b7410
|
||||
subpackages:
|
||||
- acme
|
||||
- providers/dns
|
||||
- providers/dns/auroradns
|
||||
- providers/dns/azure
|
||||
- providers/dns/cloudflare
|
||||
- providers/dns/digitalocean
|
||||
- providers/dns/dnsimple
|
||||
- providers/dns/dnsmadeeasy
|
||||
- providers/dns/dnspod
|
||||
- providers/dns/dyn
|
||||
- providers/dns/exoscale
|
||||
- providers/dns/gandi
|
||||
- providers/dns/googlecloud
|
||||
- providers/dns/linode
|
||||
- providers/dns/namecheap
|
||||
- providers/dns/ns1
|
||||
- providers/dns/ovh
|
||||
- providers/dns/pdns
|
||||
- providers/dns/rackspace
|
||||
- providers/dns/rfc2136
|
||||
- providers/dns/route53
|
||||
- providers/dns/vultr
|
||||
- name: golang.org/x/crypto
|
||||
version: d81fdb778bf2c40a91b24519d60cdc5767318829
|
||||
version: 4ed45ec682102c643324fae5dff8dab085b6c300
|
||||
subpackages:
|
||||
- bcrypt
|
||||
- blowfish
|
||||
- ocsp
|
||||
- name: golang.org/x/net
|
||||
version: b400c2eff1badec7022a8c8f5bea058b6315eed7
|
||||
version: 242b6b35177ec3909636b6cf6a47e8c2c6324b5d
|
||||
subpackages:
|
||||
- context
|
||||
- context/ctxhttp
|
||||
- http2
|
||||
- http2/hpack
|
||||
- idna
|
||||
- lex/httplex
|
||||
- proxy
|
||||
- publicsuffix
|
||||
- name: golang.org/x/oauth2
|
||||
version: 3046bc76d6dfd7d3707f6640f85e42d9c4050f50
|
||||
subpackages:
|
||||
- google
|
||||
- internal
|
||||
- jws
|
||||
- jwt
|
||||
- name: golang.org/x/sys
|
||||
version: 62bee037599929a6e9146f29d10dd5208c43507d
|
||||
version: eb2c74142fd19a79b3f237334c7384d5167b1b46
|
||||
subpackages:
|
||||
- unix
|
||||
- windows
|
||||
- name: golang.org/x/text
|
||||
version: a49bea13b776691cb1b49873e5d8df96ec74831a
|
||||
repo: https://github.com/golang/text.git
|
||||
vcs: git
|
||||
subpackages:
|
||||
- .
|
||||
- transform
|
||||
- unicode/norm
|
||||
- width
|
||||
- name: google.golang.org/api
|
||||
version: 9bf6e6e569ff057f75d9604a46c52928f17d2b54
|
||||
subpackages:
|
||||
- dns/v1
|
||||
- gensupport
|
||||
- googleapi
|
||||
- googleapi/internal/uritemplates
|
||||
- name: google.golang.org/appengine
|
||||
version: 12d5545dc1cfa6047a286d5e853841b6471f4c19
|
||||
subpackages:
|
||||
- internal
|
||||
- internal/app_identity
|
||||
- internal/base
|
||||
- internal/datastore
|
||||
- internal/log
|
||||
- internal/modules
|
||||
- internal/remote_api
|
||||
- internal/urlfetch
|
||||
- urlfetch
|
||||
- name: google.golang.org/cloud
|
||||
version: f20d6dcccb44ed49de45ae3703312cb46e627db1
|
||||
subpackages:
|
||||
- compute/metadata
|
||||
- internal
|
||||
- name: gopkg.in/fsnotify.v1
|
||||
version: 944cff21b3baf3ced9a880365682152ba577d348
|
||||
version: a8a77c9133d2d6fd8334f3260d06f60e8d80a5fb
|
||||
- name: gopkg.in/inf.v0
|
||||
version: 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
|
||||
- name: gopkg.in/ini.v1
|
||||
version: 6f66b0e091edb3c7b380f7c4f0f884274d550b67
|
||||
- name: gopkg.in/mgo.v2
|
||||
version: 22287bab4379e1fbf6002fb4eb769888f3fb224c
|
||||
version: 29cc868a5ca65f401ff318143f9408d02f4799cc
|
||||
subpackages:
|
||||
- bson
|
||||
- name: gopkg.in/ns1/ns1-go.v2
|
||||
version: d8d10b7f448291ddbdce48d4594fb1b667014c8b
|
||||
subpackages:
|
||||
- rest
|
||||
- rest/model/account
|
||||
- rest/model/data
|
||||
- rest/model/dns
|
||||
- rest/model/filter
|
||||
- rest/model/monitor
|
||||
- name: gopkg.in/square/go-jose.v1
|
||||
version: aa2e30fdd1fe9dd3394119af66451ae790d50e0d
|
||||
version: e3f973b66b91445ec816dd7411ad1b6495a5a2fc
|
||||
subpackages:
|
||||
- cipher
|
||||
- json
|
||||
- name: gopkg.in/yaml.v2
|
||||
version: bef53efd0c76e49e6de55ead051f886bea7e9420
|
||||
- name: k8s.io/client-go
|
||||
version: 1195e3a8ee1a529d53eed7c624527a68555ddf1f
|
||||
subpackages:
|
||||
- 1.5/discovery
|
||||
- 1.5/kubernetes
|
||||
- 1.5/kubernetes/typed/apps/v1alpha1
|
||||
- 1.5/kubernetes/typed/authentication/v1beta1
|
||||
- 1.5/kubernetes/typed/authorization/v1beta1
|
||||
- 1.5/kubernetes/typed/autoscaling/v1
|
||||
- 1.5/kubernetes/typed/batch/v1
|
||||
- 1.5/kubernetes/typed/certificates/v1alpha1
|
||||
- 1.5/kubernetes/typed/core/v1
|
||||
- 1.5/kubernetes/typed/extensions/v1beta1
|
||||
- 1.5/kubernetes/typed/policy/v1alpha1
|
||||
- 1.5/kubernetes/typed/rbac/v1alpha1
|
||||
- 1.5/kubernetes/typed/storage/v1beta1
|
||||
- 1.5/pkg/api
|
||||
- 1.5/pkg/api/errors
|
||||
- 1.5/pkg/api/install
|
||||
- 1.5/pkg/api/meta
|
||||
- 1.5/pkg/api/meta/metatypes
|
||||
- 1.5/pkg/api/resource
|
||||
- 1.5/pkg/api/unversioned
|
||||
- 1.5/pkg/api/v1
|
||||
- 1.5/pkg/api/validation/path
|
||||
- 1.5/pkg/apimachinery
|
||||
- 1.5/pkg/apimachinery/announced
|
||||
- 1.5/pkg/apimachinery/registered
|
||||
- 1.5/pkg/apis/apps
|
||||
- 1.5/pkg/apis/apps/install
|
||||
- 1.5/pkg/apis/apps/v1alpha1
|
||||
- 1.5/pkg/apis/authentication
|
||||
- 1.5/pkg/apis/authentication/install
|
||||
- 1.5/pkg/apis/authentication/v1beta1
|
||||
- 1.5/pkg/apis/authorization
|
||||
- 1.5/pkg/apis/authorization/install
|
||||
- 1.5/pkg/apis/authorization/v1beta1
|
||||
- 1.5/pkg/apis/autoscaling
|
||||
- 1.5/pkg/apis/autoscaling/install
|
||||
- 1.5/pkg/apis/autoscaling/v1
|
||||
- 1.5/pkg/apis/batch
|
||||
- 1.5/pkg/apis/batch/install
|
||||
- 1.5/pkg/apis/batch/v1
|
||||
- 1.5/pkg/apis/batch/v2alpha1
|
||||
- 1.5/pkg/apis/certificates
|
||||
- 1.5/pkg/apis/certificates/install
|
||||
- 1.5/pkg/apis/certificates/v1alpha1
|
||||
- 1.5/pkg/apis/extensions
|
||||
- 1.5/pkg/apis/extensions/install
|
||||
- 1.5/pkg/apis/extensions/v1beta1
|
||||
- 1.5/pkg/apis/policy
|
||||
- 1.5/pkg/apis/policy/install
|
||||
- 1.5/pkg/apis/policy/v1alpha1
|
||||
- 1.5/pkg/apis/rbac
|
||||
- 1.5/pkg/apis/rbac/install
|
||||
- 1.5/pkg/apis/rbac/v1alpha1
|
||||
- 1.5/pkg/apis/storage
|
||||
- 1.5/pkg/apis/storage/install
|
||||
- 1.5/pkg/apis/storage/v1beta1
|
||||
- 1.5/pkg/auth/user
|
||||
- 1.5/pkg/conversion
|
||||
- 1.5/pkg/conversion/queryparams
|
||||
- 1.5/pkg/fields
|
||||
- 1.5/pkg/genericapiserver/openapi/common
|
||||
- 1.5/pkg/labels
|
||||
- 1.5/pkg/runtime
|
||||
- 1.5/pkg/runtime/serializer
|
||||
- 1.5/pkg/runtime/serializer/json
|
||||
- 1.5/pkg/runtime/serializer/protobuf
|
||||
- 1.5/pkg/runtime/serializer/recognizer
|
||||
- 1.5/pkg/runtime/serializer/streaming
|
||||
- 1.5/pkg/runtime/serializer/versioning
|
||||
- 1.5/pkg/selection
|
||||
- 1.5/pkg/third_party/forked/golang/reflect
|
||||
- 1.5/pkg/types
|
||||
- 1.5/pkg/util
|
||||
- 1.5/pkg/util/cert
|
||||
- 1.5/pkg/util/clock
|
||||
- 1.5/pkg/util/errors
|
||||
- 1.5/pkg/util/flowcontrol
|
||||
- 1.5/pkg/util/framer
|
||||
- 1.5/pkg/util/integer
|
||||
- 1.5/pkg/util/intstr
|
||||
- 1.5/pkg/util/json
|
||||
- 1.5/pkg/util/labels
|
||||
- 1.5/pkg/util/net
|
||||
- 1.5/pkg/util/parsers
|
||||
- 1.5/pkg/util/rand
|
||||
- 1.5/pkg/util/runtime
|
||||
- 1.5/pkg/util/sets
|
||||
- 1.5/pkg/util/uuid
|
||||
- 1.5/pkg/util/validation
|
||||
- 1.5/pkg/util/validation/field
|
||||
- 1.5/pkg/util/wait
|
||||
- 1.5/pkg/util/yaml
|
||||
- 1.5/pkg/version
|
||||
- 1.5/pkg/watch
|
||||
- 1.5/pkg/watch/versioned
|
||||
- 1.5/plugin/pkg/client/auth
|
||||
- 1.5/plugin/pkg/client/auth/gcp
|
||||
- 1.5/plugin/pkg/client/auth/oidc
|
||||
- 1.5/rest
|
||||
- 1.5/tools/cache
|
||||
- 1.5/tools/clientcmd/api
|
||||
- 1.5/tools/metrics
|
||||
- 1.5/transport
|
||||
testImports:
|
||||
- name: github.com/Azure/go-ansiterm
|
||||
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
|
||||
@@ -345,23 +712,56 @@ testImports:
|
||||
- winterm
|
||||
- name: github.com/cloudfoundry-incubator/candiedyaml
|
||||
version: 99c3df83b51532e3615f851d8c2dbb638f5313bf
|
||||
- name: github.com/docker/libcompose
|
||||
version: d1876c1d68527a49c0aac22a0b161acc7296b740
|
||||
subpackages:
|
||||
- config
|
||||
- docker
|
||||
- docker/builder
|
||||
- docker/client
|
||||
- docker/network
|
||||
- labels
|
||||
- logger
|
||||
- lookup
|
||||
- project
|
||||
- project/events
|
||||
- project/options
|
||||
- utils
|
||||
- version
|
||||
- yaml
|
||||
- name: github.com/flynn/go-shlex
|
||||
version: 3f9db97f856818214da2e1057f8ad84803971cff
|
||||
- name: github.com/go-check/check
|
||||
version: 11d3bc7aa68e238947792f30573146a3231fc0f1
|
||||
- name: github.com/gorilla/mux
|
||||
version: 9fa818a44c2bf1396a17f9d5a3c0f6dd39d2ff8e
|
||||
version: e444e69cbd2e2e3e0749a2f3c717cec491552bbf
|
||||
- name: github.com/libkermit/compose
|
||||
version: cadc5a3b83a15790174bd7fbc75ea2529785e772
|
||||
subpackages:
|
||||
- check
|
||||
- name: github.com/libkermit/docker
|
||||
version: 55e3595409924fcfbb850811e5a7cdbe8960a0b7
|
||||
- name: github.com/libkermit/docker-check
|
||||
version: cbe0ef03b3d23070eac4d00ba8828f2cc7f7e5a3
|
||||
- name: github.com/spf13/pflag
|
||||
version: 5644820622454e71517561946e3d94b9f9db6842
|
||||
- name: github.com/opencontainers/runtime-spec
|
||||
version: 06479209bdc0d4135911688c18157bd39bd99c22
|
||||
subpackages:
|
||||
- specs-go
|
||||
- name: github.com/vbatts/tar-split
|
||||
version: bd4c5d64c3e9297f410025a3b1bd0c58f659e721
|
||||
version: 6810cedb21b2c3d0b9bb8f9af12ff2dc7a2f14df
|
||||
subpackages:
|
||||
- archive/tar
|
||||
- tar/asm
|
||||
- tar/storage
|
||||
- name: github.com/vdemeester/shakers
|
||||
version: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3
|
||||
- name: github.com/xeipuuv/gojsonpointer
|
||||
version: e0fe6f68307607d540ed8eac07a342c33fa1b54a
|
||||
- name: github.com/xeipuuv/gojsonreference
|
||||
version: e02fc20de94c78484cd5ffb007f8af96be030a45
|
||||
- name: github.com/xeipuuv/gojsonschema
|
||||
version: 00f9fafb54d2244d291b86ab63d12c38bd5c3886
|
||||
- name: golang.org/x/time
|
||||
version: a4bde12657593d5e90d0533a3e4fd95e635124cb
|
||||
subpackages:
|
||||
- rate
|
||||
|
83
glide.yaml
83
glide.yaml
@@ -6,11 +6,10 @@ import:
|
||||
- fun
|
||||
- package: github.com/Sirupsen/logrus
|
||||
- package: github.com/cenk/backoff
|
||||
- package: github.com/urfave/negroni
|
||||
- package: github.com/containous/flaeg
|
||||
version: a731c034dda967333efce5f8d276aeff11f8ff87
|
||||
- package: github.com/vulcand/oxy
|
||||
version: fcc76b52eb8568540a020b7a99e854d9d752b364
|
||||
version: f88530866c561d24a6b5aac49f76d6351b788b9f
|
||||
repo: https://github.com/containous/oxy.git
|
||||
vcs: git
|
||||
subpackages:
|
||||
@@ -23,16 +22,19 @@ import:
|
||||
- package: github.com/containous/staert
|
||||
version: 1e26a71803e428fd933f5f9c8e50a26878f53147
|
||||
- package: github.com/docker/engine-api
|
||||
version: 62043eb79d581a32ea849645277023c550732e52
|
||||
version: v0.4.0
|
||||
subpackages:
|
||||
- client
|
||||
- types
|
||||
- types/events
|
||||
- types/filters
|
||||
- package: github.com/docker/go-connections
|
||||
version: v0.2.1
|
||||
subpackages:
|
||||
- sockets
|
||||
- tlsconfig
|
||||
- package: github.com/docker/go-units
|
||||
version: 0dadbb0345b35ec7ef35e228dabb8de89a65bf52
|
||||
- package: github.com/docker/libkv
|
||||
subpackages:
|
||||
- store
|
||||
@@ -41,46 +43,34 @@ import:
|
||||
- store/etcd
|
||||
- store/zookeeper
|
||||
- package: github.com/elazarl/go-bindata-assetfs
|
||||
- package: github.com/gambol99/go-marathon
|
||||
version: a558128c87724cd7430060ef5aedf39f83937f55
|
||||
- package: github.com/containous/mux
|
||||
- package: github.com/hashicorp/consul
|
||||
subpackages:
|
||||
- api
|
||||
- package: github.com/mailgun/manners
|
||||
- package: github.com/parnurzeal/gorequest
|
||||
- package: github.com/streamrail/concurrent-map
|
||||
- package: github.com/stretchr/testify
|
||||
subpackages:
|
||||
- mock
|
||||
- package: github.com/thoas/stats
|
||||
version: 152b5d051953fdb6e45f14b6826962aadc032324
|
||||
- package: github.com/unrolled/render
|
||||
- package: github.com/vdemeester/docker-events
|
||||
version: be74d4929ec1ad118df54349fda4b0cba60f849b
|
||||
- package: github.com/vulcand/vulcand
|
||||
version: 42492a3a85e294bdbdd1bcabb8c12769a81ea284
|
||||
subpackages:
|
||||
- plugin/rewrite
|
||||
- package: github.com/xenolf/lego
|
||||
version: b2fad6198110326662e9e356a97199078a4a775c
|
||||
version: 0e2937900b224325f4476745a9b53aef246b7410
|
||||
subpackages:
|
||||
- acme
|
||||
- package: golang.org/x/net
|
||||
subpackages:
|
||||
- context
|
||||
- package: gopkg.in/fsnotify.v1
|
||||
- package: github.com/libkermit/compose
|
||||
version: cadc5a3b83a15790174bd7fbc75ea2529785e772
|
||||
- package: github.com/libkermit/docker
|
||||
version: 55e3595409924fcfbb850811e5a7cdbe8960a0b7
|
||||
- package: github.com/docker/docker
|
||||
version: 534753663161334baba06f13b8efa4cad22b5bc5
|
||||
version: v1.13.0
|
||||
subpackages:
|
||||
- namesgenerator
|
||||
- package: github.com/go-check/check
|
||||
- package: github.com/docker/libcompose
|
||||
version: d1876c1d68527a49c0aac22a0b161acc7296b740
|
||||
- package: github.com/mattn/go-shellwords
|
||||
- package: github.com/vdemeester/shakers
|
||||
- package: github.com/ryanuber/go-glob
|
||||
- package: github.com/mesos/mesos-go
|
||||
subpackages:
|
||||
@@ -89,24 +79,63 @@ import:
|
||||
- upid
|
||||
- mesosutil
|
||||
- detector
|
||||
- package: github.com/jarcoal/httpmock
|
||||
- package: github.com/miekg/dns
|
||||
version: 8060d9f51305bbe024b99679454e62f552cd0b0b
|
||||
- package: github.com/mesosphere/mesos-dns
|
||||
version: b47dc4c19f215e98da687b15b4c64e70f629bea5
|
||||
repo: https://github.com/containous/mesos-dns.git
|
||||
vcs: git
|
||||
- package: github.com/tv42/zbase32
|
||||
- package: github.com/abbot/go-http-auth
|
||||
- package: github.com/miekg/dns
|
||||
version: 5d001d020961ae1c184f9f8152fdc73810481677
|
||||
- package: github.com/NYTimes/gziphandler
|
||||
- package: github.com/docker/leadership
|
||||
- package: github.com/satori/go.uuid
|
||||
version: ^1.1.0
|
||||
- package: github.com/mitchellh/mapstructure
|
||||
version: f3009df150dadf309fdee4a54ed65c124afad715
|
||||
- package: k8s.io/client-go
|
||||
version: ^v1.5.0
|
||||
- package: github.com/gambol99/go-marathon
|
||||
version: ^0.5.1
|
||||
- package: github.com/ArthurHlt/go-eureka-client
|
||||
subpackages:
|
||||
- eureka
|
||||
- package: github.com/coreos/go-systemd
|
||||
version: v12
|
||||
version: v14
|
||||
subpackages:
|
||||
- daemon
|
||||
- package: github.com/google/go-github
|
||||
- package: github.com/hashicorp/go-version
|
||||
- package: github.com/hashicorp/go-version
|
||||
- package: github.com/mvdan/xurls
|
||||
- package: github.com/go-kit/kit
|
||||
version: v0.3.0
|
||||
subpackages:
|
||||
- metrics
|
||||
- package: github.com/eapache/channels
|
||||
version: v1.1.0
|
||||
- package: golang.org/x/net
|
||||
version: 242b6b35177ec3909636b6cf6a47e8c2c6324b5d
|
||||
subpackages:
|
||||
- http2
|
||||
- context
|
||||
- package: github.com/docker/distribution
|
||||
version: v2.6.0
|
||||
- package: github.com/aws/aws-sdk-go
|
||||
version: v1.6.18
|
||||
subpackages:
|
||||
- aws
|
||||
- aws/credentials
|
||||
- aws/defaults
|
||||
- aws/ec2metadata
|
||||
- aws/endpoints
|
||||
- aws/request
|
||||
- aws/session
|
||||
- service/ec2
|
||||
- service/ecs
|
||||
- package: cloud.google.com/go
|
||||
version: v0.6.0
|
||||
subpackages:
|
||||
- compute/metadata
|
||||
- package: github.com/gogo/protobuf
|
||||
version: v0.3
|
||||
subpackages:
|
||||
- proto
|
||||
- package: github.com/rancher/go-rancher
|
||||
version: 5b8f6cc26b355ba03d7611fce3844155b7baf05b
|
||||
|
117
healthcheck/healthcheck.go
Normal file
117
healthcheck/healthcheck.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/vulcand/oxy/roundrobin"
|
||||
)
|
||||
|
||||
var singleton *HealthCheck
|
||||
var once sync.Once
|
||||
|
||||
// GetHealthCheck Get HealtchCheck Singleton
|
||||
func GetHealthCheck() *HealthCheck {
|
||||
once.Do(func() {
|
||||
singleton = newHealthCheck()
|
||||
})
|
||||
return singleton
|
||||
}
|
||||
|
||||
// BackendHealthCheck HealthCheck configuration for a backend
|
||||
type BackendHealthCheck struct {
|
||||
Path string
|
||||
Interval time.Duration
|
||||
DisabledURLs []*url.URL
|
||||
lb loadBalancer
|
||||
}
|
||||
|
||||
var launch = false
|
||||
|
||||
//HealthCheck struct
|
||||
type HealthCheck struct {
|
||||
Backends map[string]*BackendHealthCheck
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type loadBalancer interface {
|
||||
RemoveServer(u *url.URL) error
|
||||
UpsertServer(u *url.URL, options ...roundrobin.ServerOption) error
|
||||
Servers() []*url.URL
|
||||
}
|
||||
|
||||
func newHealthCheck() *HealthCheck {
|
||||
return &HealthCheck{make(map[string]*BackendHealthCheck), nil}
|
||||
}
|
||||
|
||||
// NewBackendHealthCheck Instantiate a new BackendHealthCheck
|
||||
func NewBackendHealthCheck(URL string, interval time.Duration, lb loadBalancer) *BackendHealthCheck {
|
||||
return &BackendHealthCheck{URL, interval, nil, lb}
|
||||
}
|
||||
|
||||
//SetBackendsConfiguration set backends configuration
|
||||
func (hc *HealthCheck) SetBackendsConfiguration(parentCtx context.Context, backends map[string]*BackendHealthCheck) {
|
||||
hc.Backends = backends
|
||||
if hc.cancel != nil {
|
||||
hc.cancel()
|
||||
}
|
||||
ctx, cancel := context.WithCancel(parentCtx)
|
||||
hc.cancel = cancel
|
||||
hc.execute(ctx)
|
||||
}
|
||||
|
||||
func (hc *HealthCheck) execute(ctx context.Context) {
|
||||
for backendID, backend := range hc.Backends {
|
||||
currentBackend := backend
|
||||
currentBackendID := backendID
|
||||
safe.Go(func() {
|
||||
for {
|
||||
ticker := time.NewTicker(currentBackend.Interval)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Debugf("Stopping all current Healthcheck goroutines")
|
||||
return
|
||||
case <-ticker.C:
|
||||
log.Debugf("Refreshing Healthcheck for currentBackend %s ", currentBackendID)
|
||||
enabledURLs := currentBackend.lb.Servers()
|
||||
var newDisabledURLs []*url.URL
|
||||
for _, url := range currentBackend.DisabledURLs {
|
||||
if checkHealth(url, currentBackend.Path) {
|
||||
log.Debugf("HealthCheck is up [%s]: Upsert in server list", url.String())
|
||||
currentBackend.lb.UpsertServer(url, roundrobin.Weight(1))
|
||||
} else {
|
||||
newDisabledURLs = append(newDisabledURLs, url)
|
||||
}
|
||||
}
|
||||
currentBackend.DisabledURLs = newDisabledURLs
|
||||
|
||||
for _, url := range enabledURLs {
|
||||
if !checkHealth(url, currentBackend.Path) {
|
||||
log.Debugf("HealthCheck has failed [%s]: Remove from server list", url.String())
|
||||
currentBackend.lb.RemoveServer(url)
|
||||
currentBackend.DisabledURLs = append(currentBackend.DisabledURLs, url)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkHealth(serverURL *url.URL, path string) bool {
|
||||
timeout := time.Duration(5 * time.Second)
|
||||
client := http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
resp, err := client.Get(serverURL.String() + path)
|
||||
if err != nil || resp.StatusCode != 200 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
92
integration/acme_test.go
Normal file
92
integration/acme_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/go-check/check"
|
||||
|
||||
"errors"
|
||||
"github.com/containous/traefik/integration/utils"
|
||||
checker "github.com/vdemeester/shakers"
|
||||
)
|
||||
|
||||
// ACME test suites (using libcompose)
|
||||
type AcmeSuite struct {
|
||||
BaseSuite
|
||||
}
|
||||
|
||||
func (s *AcmeSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "boulder")
|
||||
s.composeProject.Start(c)
|
||||
|
||||
boulderHost := s.composeProject.Container(c, "boulder").NetworkSettings.IPAddress
|
||||
|
||||
// wait for boulder
|
||||
err := utils.Try(120*time.Second, func() error {
|
||||
resp, err := http.Get("http://" + boulderHost + ":4000/directory")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return errors.New("Expected http 200 from boulder")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
c.Assert(err, checker.IsNil)
|
||||
}
|
||||
|
||||
func (s *AcmeSuite) TearDownSuite(c *check.C) {
|
||||
// shutdown and delete compose project
|
||||
if s.composeProject != nil {
|
||||
s.composeProject.Stop(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AcmeSuite) TestRetrieveAcmeCertificate(c *check.C) {
|
||||
boulderHost := s.composeProject.Container(c, "boulder").NetworkSettings.IPAddress
|
||||
file := s.adaptFile(c, "fixtures/acme/acme.toml", struct{ BoulderHost string }{boulderHost})
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
backend := startTestServer("9010", 200)
|
||||
defer backend.Close()
|
||||
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
// wait for traefik (generating acme account take some seconds)
|
||||
err = utils.Try(30*time.Second, func() error {
|
||||
_, err := client.Get("https://127.0.0.1:5001")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
tr = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: "traefik.acme.wtf",
|
||||
},
|
||||
}
|
||||
client = &http.Client{Transport: tr}
|
||||
req, _ := http.NewRequest("GET", "https://127.0.0.1:5001/", nil)
|
||||
req.Host = "traefik.acme.wtf"
|
||||
req.Header.Set("Host", "traefik.acme.wtf")
|
||||
req.Header.Set("Accept", "*/*")
|
||||
resp, err := client.Do(req)
|
||||
c.Assert(err, checker.IsNil)
|
||||
// Expected a 200
|
||||
c.Assert(resp.StatusCode, checker.Equals, 200)
|
||||
}
|
@@ -5,12 +5,12 @@ import (
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"context"
|
||||
"github.com/containous/staert"
|
||||
"github.com/docker/libkv"
|
||||
"github.com/docker/libkv/store"
|
||||
"github.com/docker/libkv/store/consul"
|
||||
"github.com/go-check/check"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"errors"
|
||||
"github.com/containous/traefik/cluster"
|
||||
|
@@ -110,7 +110,7 @@ func (s *DockerSuite) TestDefaultDockerContainers(c *check.C) {
|
||||
client := &http.Client{}
|
||||
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)
|
||||
req.Host = fmt.Sprintf("%s.docker.localhost", strings.Replace(name, "_", "-", -1))
|
||||
resp, err := client.Do(req)
|
||||
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
111
integration/eureka_test.go
Normal file
111
integration/eureka_test.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/containous/traefik/integration/utils"
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
)
|
||||
|
||||
// Eureka test suites (using libcompose)
|
||||
type EurekaSuite struct{ BaseSuite }
|
||||
|
||||
func (s *EurekaSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "eureka")
|
||||
s.composeProject.Start(c)
|
||||
|
||||
}
|
||||
|
||||
func (s *EurekaSuite) TestSimpleConfiguration(c *check.C) {
|
||||
|
||||
eurekaHost := s.composeProject.Container(c, "eureka").NetworkSettings.IPAddress
|
||||
whoami1Host := s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress
|
||||
|
||||
file := s.adaptFile(c, "fixtures/eureka/simple.toml", struct{ EurekaHost string }{eurekaHost})
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
eurekaURL := "http://" + eurekaHost + ":8761/eureka/apps"
|
||||
|
||||
// wait for eureka
|
||||
err = utils.TryRequest(eurekaURL, 60*time.Second, func(res *http.Response) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
eurekaTemplate := `
|
||||
{
|
||||
"instance": {
|
||||
"hostName": "{{ .IP }}",
|
||||
"app": "{{ .ID }}",
|
||||
"ipAddr": "{{ .IP }}",
|
||||
"status": "UP",
|
||||
"port": {
|
||||
"$": {{ .Port }},
|
||||
"@enabled": "true"
|
||||
},
|
||||
"dataCenterInfo": {
|
||||
"name": "MyOwn"
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
tmpl, err := template.New("eurekaTemlate").Parse(eurekaTemplate)
|
||||
c.Assert(err, checker.IsNil)
|
||||
buf := new(bytes.Buffer)
|
||||
templateVars := map[string]string{
|
||||
"ID": "tests-integration-traefik",
|
||||
"IP": whoami1Host,
|
||||
"Port": "80",
|
||||
}
|
||||
// add in eureka
|
||||
err = tmpl.Execute(buf, templateVars)
|
||||
resp, err := http.Post(eurekaURL+"/tests-integration-traefik", "application/json", strings.NewReader(buf.String()))
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 204)
|
||||
|
||||
// wait for traefik
|
||||
err = utils.TryRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, func(res *http.Response) error {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.Contains(string(body), "Host:tests-integration-traefik") {
|
||||
return errors.New("Incorrect traefik config")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
req.Host = "tests-integration-traefik"
|
||||
resp, err = client.Do(req)
|
||||
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 200)
|
||||
|
||||
// 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)
|
||||
}
|
32
integration/fixtures/acme/acme.toml
Normal file
32
integration/fixtures/acme/acme.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
logLevel = "DEBUG"
|
||||
|
||||
defaultEntryPoints = ["http", "https"]
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8080"
|
||||
[entryPoints.https]
|
||||
address = ":5001"
|
||||
[entryPoints.https.tls]
|
||||
|
||||
|
||||
[acme]
|
||||
email = "test@traefik.io"
|
||||
storage = "/dev/null"
|
||||
entryPoint = "https"
|
||||
onDemand = true
|
||||
caServer = "http://{{.BoulderHost}}:4000/directory"
|
||||
|
||||
[file]
|
||||
|
||||
[backends]
|
||||
[backends.backend]
|
||||
[backends.backend.servers.server1]
|
||||
url = "http://127.0.0.1:9010"
|
||||
|
||||
|
||||
[frontends]
|
||||
[frontends.frontend]
|
||||
backend = "backend"
|
||||
[frontends.frontend.routes.test]
|
||||
rule = "Host:traefik.acme.wtf"
|
14
integration/fixtures/eureka/simple.toml
Normal file
14
integration/fixtures/eureka/simple.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
|
||||
[eureka]
|
||||
endpoint = "http://{{.EurekaHost}}:8761/eureka"
|
||||
delay = "1s"
|
||||
[web]
|
||||
address = ":8080"
|
27
integration/fixtures/healthcheck/simple.toml
Normal file
27
integration/fixtures/healthcheck/simple.toml
Normal file
@@ -0,0 +1,27 @@
|
||||
defaultEntryPoints = ["http"]
|
||||
|
||||
logLevel = "DEBUG"
|
||||
|
||||
[entryPoints]
|
||||
[entryPoints.http]
|
||||
address = ":8000"
|
||||
|
||||
[web]
|
||||
address = ":8080"
|
||||
|
||||
[file]
|
||||
[backends]
|
||||
[backends.backend1]
|
||||
[backends.backend1.healthcheck]
|
||||
url = "/health"
|
||||
interval = "1s"
|
||||
[backends.backend1.servers.server1]
|
||||
url = "http://{{.Server1}}:80"
|
||||
[backends.backend1.servers.server2]
|
||||
url = "http://{{.Server2}}:80"
|
||||
|
||||
[frontends]
|
||||
[frontends.frontend1]
|
||||
backend = "backend1"
|
||||
[frontends.frontend1.routes.test_1]
|
||||
rule = "Host:test.localhost"
|
295
integration/glide.lock
generated
Normal file
295
integration/glide.lock
generated
Normal file
@@ -0,0 +1,295 @@
|
||||
hash: c53f57a45247b08a91f127ece494d49f1b7fee8c5f75be87ab12e27aa92d065f
|
||||
updated: 2016-11-17T16:23:56.727970904Z
|
||||
imports:
|
||||
- name: github.com/cenk/backoff
|
||||
version: 8edc80b07f38c27352fb186d971c628a6c32552b
|
||||
testImports:
|
||||
- name: github.com/ArthurHlt/go-eureka-client
|
||||
version: ba361cd0f9f571b4e871421423d2f02f5689c3d2
|
||||
subpackages:
|
||||
- eureka
|
||||
- name: github.com/ArthurHlt/gominlog
|
||||
version: 068c01ce147ad68fca25ef3fa29ae5395ae273ab
|
||||
- name: github.com/Azure/go-ansiterm
|
||||
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
|
||||
subpackages:
|
||||
- winterm
|
||||
- name: github.com/boltdb/bolt
|
||||
version: f4c032d907f61f08dba2d719c58f108a1abb8e81
|
||||
- name: github.com/BurntSushi/toml
|
||||
version: 99064174e013895bbd9b025c31100bd1d9b590ca
|
||||
- name: github.com/BurntSushi/ty
|
||||
version: 6add9cd6ad42d389d6ead1dde60b4ad71e46fd74
|
||||
subpackages:
|
||||
- fun
|
||||
- name: github.com/cloudfoundry-incubator/candiedyaml
|
||||
version: 99c3df83b51532e3615f851d8c2dbb638f5313bf
|
||||
- name: github.com/containous/flaeg
|
||||
version: a731c034dda967333efce5f8d276aeff11f8ff87
|
||||
- name: github.com/containous/staert
|
||||
version: 92329254783dc01174f03302d51d7cf2c9ff84cf
|
||||
- name: github.com/containous/traefik
|
||||
version: 15732269da23c35524bf7cabea5857e4c5f63881
|
||||
subpackages:
|
||||
- autogen
|
||||
- cluster
|
||||
- job
|
||||
- log
|
||||
- provider
|
||||
- provider/k8s
|
||||
- safe
|
||||
- types
|
||||
- version
|
||||
- name: github.com/coreos/etcd
|
||||
version: c400d05d0aa73e21e431c16145e558d624098018
|
||||
subpackages:
|
||||
- Godeps/_workspace/src/github.com/ugorji/go/codec
|
||||
- Godeps/_workspace/src/golang.org/x/net/context
|
||||
- client
|
||||
- pkg/pathutil
|
||||
- pkg/types
|
||||
- name: github.com/daviddengcn/go-colortext
|
||||
version: 3b18c8575a432453d41fdafb340099fff5bba2f7
|
||||
- name: github.com/docker/distribution
|
||||
version: 99cb7c0946d2f5a38015443e515dc916295064d7
|
||||
subpackages:
|
||||
- context
|
||||
- digest
|
||||
- reference
|
||||
- registry/api/errcode
|
||||
- registry/api/v2
|
||||
- registry/client
|
||||
- registry/client/auth
|
||||
- registry/client/transport
|
||||
- registry/storage/cache
|
||||
- registry/storage/cache/memory
|
||||
- uuid
|
||||
- name: github.com/docker/docker
|
||||
version: 534753663161334baba06f13b8efa4cad22b5bc5
|
||||
subpackages:
|
||||
- api/types/backend
|
||||
- builder
|
||||
- builder/dockerignore
|
||||
- cliconfig
|
||||
- cliconfig/configfile
|
||||
- daemon/graphdriver
|
||||
- image
|
||||
- image/v1
|
||||
- layer
|
||||
- opts
|
||||
- pkg/archive
|
||||
- pkg/chrootarchive
|
||||
- pkg/fileutils
|
||||
- pkg/gitutils
|
||||
- pkg/homedir
|
||||
- pkg/httputils
|
||||
- pkg/idtools
|
||||
- pkg/ioutils
|
||||
- pkg/jsonlog
|
||||
- pkg/jsonmessage
|
||||
- pkg/longpath
|
||||
- pkg/mflag
|
||||
- pkg/mount
|
||||
- pkg/namesgenerator
|
||||
- pkg/plugins
|
||||
- pkg/plugins/transport
|
||||
- pkg/pools
|
||||
- pkg/progress
|
||||
- pkg/promise
|
||||
- pkg/random
|
||||
- pkg/reexec
|
||||
- pkg/signal
|
||||
- pkg/stdcopy
|
||||
- pkg/streamformatter
|
||||
- pkg/stringid
|
||||
- pkg/symlink
|
||||
- pkg/system
|
||||
- pkg/tarsum
|
||||
- pkg/term
|
||||
- pkg/term/windows
|
||||
- pkg/urlutil
|
||||
- reference
|
||||
- registry
|
||||
- runconfig/opts
|
||||
- name: github.com/docker/engine-api
|
||||
version: 62043eb79d581a32ea849645277023c550732e52
|
||||
subpackages:
|
||||
- client
|
||||
- client/transport
|
||||
- client/transport/cancellable
|
||||
- types
|
||||
- types/blkiodev
|
||||
- types/container
|
||||
- types/events
|
||||
- types/filters
|
||||
- types/network
|
||||
- types/reference
|
||||
- types/registry
|
||||
- types/strslice
|
||||
- types/swarm
|
||||
- types/time
|
||||
- types/versions
|
||||
- name: github.com/docker/go-connections
|
||||
version: 988efe982fdecb46f01d53465878ff1f2ff411ce
|
||||
subpackages:
|
||||
- nat
|
||||
- sockets
|
||||
- tlsconfig
|
||||
- name: github.com/docker/go-units
|
||||
version: f2145db703495b2e525c59662db69a7344b00bb8
|
||||
- name: github.com/docker/leadership
|
||||
version: 0a913e2d71a12fd14a028452435cb71ac8d82cb6
|
||||
- name: github.com/docker/libcompose
|
||||
version: d1876c1d68527a49c0aac22a0b161acc7296b740
|
||||
subpackages:
|
||||
- config
|
||||
- docker
|
||||
- docker/builder
|
||||
- docker/client
|
||||
- docker/network
|
||||
- labels
|
||||
- logger
|
||||
- lookup
|
||||
- project
|
||||
- project/events
|
||||
- project/options
|
||||
- utils
|
||||
- version
|
||||
- yaml
|
||||
- name: github.com/docker/libkv
|
||||
version: 3fce6a0f26e07da3eac45796a8e255547a47a750
|
||||
subpackages:
|
||||
- store
|
||||
- store/boltdb
|
||||
- store/consul
|
||||
- store/etcd
|
||||
- store/zookeeper
|
||||
- name: github.com/donovanhide/eventsource
|
||||
version: fd1de70867126402be23c306e1ce32828455d85b
|
||||
- name: github.com/flynn/go-shlex
|
||||
version: 3f9db97f856818214da2e1057f8ad84803971cff
|
||||
- name: github.com/gambol99/go-marathon
|
||||
version: a558128c87724cd7430060ef5aedf39f83937f55
|
||||
- name: github.com/go-check/check
|
||||
version: 11d3bc7aa68e238947792f30573146a3231fc0f1
|
||||
- name: github.com/gogo/protobuf
|
||||
version: 43ab7f0ec7b6d072e0368bd537ffefe74ed30198
|
||||
subpackages:
|
||||
- proto
|
||||
- name: github.com/golang/glog
|
||||
version: fca8c8854093a154ff1eb580aae10276ad6b1b5f
|
||||
- name: github.com/google/go-querystring
|
||||
version: 9235644dd9e52eeae6fa48efd539fdc351a0af53
|
||||
subpackages:
|
||||
- query
|
||||
- name: github.com/gorilla/context
|
||||
version: 14f550f51af52180c2eefed15e5fd18d63c0a64a
|
||||
- name: github.com/gorilla/mux
|
||||
version: e444e69cbd2e2e3e0749a2f3c717cec491552bbf
|
||||
- name: github.com/hashicorp/consul
|
||||
version: d8e2fb7dd594163e25a89bc52c1a4613f5c5bfb8
|
||||
subpackages:
|
||||
- api
|
||||
- name: github.com/hashicorp/go-cleanhttp
|
||||
version: ad28ea4487f05916463e2423a55166280e8254b5
|
||||
- name: github.com/hashicorp/serf
|
||||
version: 598c54895cc5a7b1a24a398d635e8c0ea0959870
|
||||
subpackages:
|
||||
- coordinate
|
||||
- name: github.com/libkermit/compose
|
||||
version: cadc5a3b83a15790174bd7fbc75ea2529785e772
|
||||
subpackages:
|
||||
- check
|
||||
- name: github.com/libkermit/docker
|
||||
version: 55e3595409924fcfbb850811e5a7cdbe8960a0b7
|
||||
- name: github.com/libkermit/docker-check
|
||||
version: cbe0ef03b3d23070eac4d00ba8828f2cc7f7e5a3
|
||||
- name: github.com/mattn/go-shellwords
|
||||
version: 525bedee691b5a8df547cb5cf9f86b7fb1883e24
|
||||
- name: github.com/mesos/mesos-go
|
||||
version: 068d5470506e3780189fe607af40892814197c5e
|
||||
subpackages:
|
||||
- detector
|
||||
- detector/zoo
|
||||
- mesosproto
|
||||
- mesosutil
|
||||
- upid
|
||||
- name: github.com/mesosphere/mesos-dns
|
||||
version: b47dc4c19f215e98da687b15b4c64e70f629bea5
|
||||
repo: https://github.com/containous/mesos-dns.git
|
||||
vcs: git
|
||||
subpackages:
|
||||
- detect
|
||||
- errorutil
|
||||
- logging
|
||||
- models
|
||||
- records
|
||||
- records/labels
|
||||
- records/state
|
||||
- util
|
||||
- name: github.com/Microsoft/go-winio
|
||||
version: ce2922f643c8fd76b46cadc7f404a06282678b34
|
||||
- name: github.com/miekg/dns
|
||||
version: 5d001d020961ae1c184f9f8152fdc73810481677
|
||||
- name: github.com/mitchellh/mapstructure
|
||||
version: ca63d7c062ee3c9f34db231e352b60012b4fd0c1
|
||||
- name: github.com/moul/http2curl
|
||||
version: b1479103caacaa39319f75e7f57fc545287fca0d
|
||||
- name: github.com/ogier/pflag
|
||||
version: 45c278ab3607870051a2ea9040bb85fcb8557481
|
||||
- name: github.com/opencontainers/runc
|
||||
version: ba1568de399395774ad84c2ace65937814c542ed
|
||||
subpackages:
|
||||
- libcontainer/user
|
||||
- name: github.com/parnurzeal/gorequest
|
||||
version: e30af16d4e485943aab0b0885ad6bdbb8c0d3dc7
|
||||
- name: github.com/ryanuber/go-glob
|
||||
version: 572520ed46dbddaed19ea3d9541bdd0494163693
|
||||
- name: github.com/samuel/go-zookeeper
|
||||
version: 87e1bca4477a3cc767ca71be023ced183d74e538
|
||||
subpackages:
|
||||
- zk
|
||||
- name: github.com/satori/go.uuid
|
||||
version: 879c5887cd475cd7864858769793b2ceb0d44feb
|
||||
- name: github.com/Sirupsen/logrus
|
||||
version: 3ec0642a7fb6488f65b06f9040adc67e3990296a
|
||||
- name: github.com/spf13/pflag
|
||||
version: 5644820622454e71517561946e3d94b9f9db6842
|
||||
- name: github.com/stretchr/objx
|
||||
version: cbeaeb16a013161a98496fad62933b1d21786672
|
||||
- name: github.com/stretchr/testify
|
||||
version: b8dc1cecf15bdaf1988d9e87aa7cd98d899a06d6
|
||||
subpackages:
|
||||
- assert
|
||||
- mock
|
||||
- name: github.com/tv42/zbase32
|
||||
version: 03389da7e0bf9844767f82690f4d68fc097a1306
|
||||
- name: github.com/vbatts/tar-split
|
||||
version: bd4c5d64c3e9297f410025a3b1bd0c58f659e721
|
||||
subpackages:
|
||||
- archive/tar
|
||||
- tar/asm
|
||||
- tar/storage
|
||||
- name: github.com/vdemeester/docker-events
|
||||
version: be74d4929ec1ad118df54349fda4b0cba60f849b
|
||||
- name: github.com/vdemeester/shakers
|
||||
version: 24d7f1d6a71aa5d9cbe7390e4afb66b7eef9e1b3
|
||||
- name: github.com/xeipuuv/gojsonpointer
|
||||
version: e0fe6f68307607d540ed8eac07a342c33fa1b54a
|
||||
- name: github.com/xeipuuv/gojsonreference
|
||||
version: e02fc20de94c78484cd5ffb007f8af96be030a45
|
||||
- name: github.com/xeipuuv/gojsonschema
|
||||
version: 00f9fafb54d2244d291b86ab63d12c38bd5c3886
|
||||
- name: golang.org/x/net
|
||||
version: db8e4de5b2d6653f66aea53094624468caad15d2
|
||||
subpackages:
|
||||
- context
|
||||
- proxy
|
||||
- publicsuffix
|
||||
- name: golang.org/x/sys
|
||||
version: 9c60d1c508f5134d1ca726b4641db998f2523357
|
||||
subpackages:
|
||||
- unix
|
||||
- windows
|
||||
- name: gopkg.in/fsnotify.v1
|
||||
version: 944cff21b3baf3ced9a880365682152ba577d348
|
33
integration/glide.yaml
Normal file
33
integration/glide.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
package: github.com/containous/traefik/integration
|
||||
import:
|
||||
- package: github.com/cenk/backoff
|
||||
testImport:
|
||||
- package: github.com/containous/staert
|
||||
version: 92329254783dc01174f03302d51d7cf2c9ff84cf
|
||||
- package: github.com/docker/docker
|
||||
version: 534753663161334baba06f13b8efa4cad22b5bc5
|
||||
subpackages:
|
||||
- pkg/namesgenerator
|
||||
- package: github.com/docker/libkv
|
||||
subpackages:
|
||||
- store
|
||||
- store/consul
|
||||
- store/etcd
|
||||
- package: github.com/go-check/check
|
||||
- package: github.com/hashicorp/consul
|
||||
subpackages:
|
||||
- api
|
||||
- package: github.com/libkermit/compose
|
||||
version: cadc5a3b83a15790174bd7fbc75ea2529785e772
|
||||
subpackages:
|
||||
- check
|
||||
- package: github.com/libkermit/docker
|
||||
version: 55e3595409924fcfbb850811e5a7cdbe8960a0b7
|
||||
- package: github.com/libkermit/docker-check
|
||||
- package: github.com/mattn/go-shellwords
|
||||
- package: github.com/vdemeester/shakers
|
||||
- package: golang.org/x/net
|
||||
subpackages:
|
||||
- context
|
||||
- package: github.com/spf13/pflag
|
||||
version: 5644820622454e71517561946e3d94b9f9db6842
|
91
integration/healthcheck_test.go
Normal file
91
integration/healthcheck_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containous/traefik/integration/utils"
|
||||
"github.com/go-check/check"
|
||||
|
||||
checker "github.com/vdemeester/shakers"
|
||||
)
|
||||
|
||||
// HealchCheck test suites (using libcompose)
|
||||
type HealchCheckSuite struct{ BaseSuite }
|
||||
|
||||
func (s *HealchCheckSuite) SetUpSuite(c *check.C) {
|
||||
s.createComposeProject(c, "healthcheck")
|
||||
s.composeProject.Start(c)
|
||||
|
||||
}
|
||||
|
||||
func (s *HealchCheckSuite) TestSimpleConfiguration(c *check.C) {
|
||||
|
||||
whoami1Host := s.composeProject.Container(c, "whoami1").NetworkSettings.IPAddress
|
||||
whoami2Host := s.composeProject.Container(c, "whoami2").NetworkSettings.IPAddress
|
||||
|
||||
file := s.adaptFile(c, "fixtures/healthcheck/simple.toml", struct {
|
||||
Server1 string
|
||||
Server2 string
|
||||
}{whoami1Host, whoami2Host})
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command(traefikBinary, "--configFile="+file)
|
||||
|
||||
err := cmd.Start()
|
||||
c.Assert(err, checker.IsNil)
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
// wait for traefik
|
||||
err = utils.TryRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, func(res *http.Response) error {
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.Contains(string(body), "Host:test.localhost") {
|
||||
return errors.New("Incorrect traefik config: " + string(body))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/health", nil)
|
||||
c.Assert(err, checker.IsNil)
|
||||
req.Host = "test.localhost"
|
||||
|
||||
resp, err := client.Do(req)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 200)
|
||||
|
||||
resp, err = client.Do(req)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 200)
|
||||
|
||||
healthReq, err := http.NewRequest("POST", "http://"+whoami1Host+"/health", bytes.NewBuffer([]byte("500")))
|
||||
c.Assert(err, checker.IsNil)
|
||||
_, err = client.Do(healthReq)
|
||||
c.Assert(err, checker.IsNil)
|
||||
|
||||
time.Sleep(time.Second * 3)
|
||||
|
||||
resp, err = client.Do(req)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 200)
|
||||
|
||||
resp, err = client.Do(req)
|
||||
c.Assert(err, checker.IsNil)
|
||||
c.Assert(resp.StatusCode, checker.Equals, 200)
|
||||
|
||||
// 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)
|
||||
}
|
@@ -4,6 +4,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -33,6 +34,8 @@ func init() {
|
||||
check.Suite(&MarathonSuite{})
|
||||
check.Suite(&ConstraintSuite{})
|
||||
check.Suite(&MesosSuite{})
|
||||
check.Suite(&EurekaSuite{})
|
||||
check.Suite(&AcmeSuite{})
|
||||
}
|
||||
|
||||
var traefikBinary = "../dist/traefik"
|
||||
@@ -51,6 +54,18 @@ func (s *BaseSuite) TearDownSuite(c *check.C) {
|
||||
func (s *BaseSuite) createComposeProject(c *check.C, name string) {
|
||||
projectName := fmt.Sprintf("integration-test-%s", name)
|
||||
composeFile := fmt.Sprintf("resources/compose/%s.yml", name)
|
||||
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
c.Assert(err, checker.IsNil)
|
||||
for _, addr := range addrs {
|
||||
ip, _, err := net.ParseCIDR(addr.String())
|
||||
c.Assert(err, checker.IsNil)
|
||||
if !ip.IsLoopback() && ip.To4() != nil {
|
||||
os.Setenv("DOCKER_HOST_IP", ip.String())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
s.composeProject = compose.CreateProject(c, projectName, composeFile)
|
||||
}
|
||||
|
||||
|
44
integration/resources/compose/boulder.yml
Normal file
44
integration/resources/compose/boulder.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
boulder:
|
||||
image: containous/boulder:release
|
||||
environment:
|
||||
FAKE_DNS: ${DOCKER_HOST_IP}
|
||||
PKCS11_PROXY_SOCKET: tcp://boulder-hsm:5657
|
||||
extra_hosts:
|
||||
- le.wtf:127.0.0.1
|
||||
- boulder:127.0.0.1
|
||||
ports:
|
||||
- 4000:4000 # ACME
|
||||
- 4002:4002 # OCSP
|
||||
- 4003:4003 # OCSP
|
||||
- 4500:4500 # ct-test-srv
|
||||
- 8000:8000 # debug ports
|
||||
- 8001:8001
|
||||
- 8002:8002
|
||||
- 8003:8003
|
||||
- 8004:8004
|
||||
- 8055:8055 # dns-test-srv updates
|
||||
- 9380:9380 # mail-test-srv
|
||||
- 9381:9381 # mail-test-srv
|
||||
links:
|
||||
- bhsm:boulder-hsm
|
||||
- bmysql:boulder-mysql
|
||||
- brabbitmq:boulder-rabbitmq
|
||||
|
||||
bhsm:
|
||||
# To minimize the fetching of various layers this should match
|
||||
# the FROM image and tag in boulder/Dockerfile
|
||||
image: letsencrypt/boulder-tools:2016-11-02
|
||||
environment:
|
||||
PKCS11_DAEMON_SOCKET: tcp://0.0.0.0:5657
|
||||
command: /usr/local/bin/pkcs11-daemon /usr/lib/softhsm/libsofthsm.so
|
||||
expose:
|
||||
- 5657
|
||||
bmysql:
|
||||
image: mariadb:10.1
|
||||
environment:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
|
||||
log_driver: none
|
||||
brabbitmq:
|
||||
image: rabbitmq:3
|
||||
environment:
|
||||
RABBITMQ_NODE_IP_ADDRESS: "0.0.0.0"
|
5
integration/resources/compose/eureka.yml
Normal file
5
integration/resources/compose/eureka.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
eureka:
|
||||
image: springcloud/eureka
|
||||
|
||||
whoami1:
|
||||
image: emilevauge/whoami
|
5
integration/resources/compose/healthcheck.yml
Normal file
5
integration/resources/compose/healthcheck.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
whoami1:
|
||||
image: emilevauge/whoami
|
||||
|
||||
whoami2:
|
||||
image: emilevauge/whoami
|
@@ -1,8 +1,9 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"github.com/cenk/backoff"
|
||||
"time"
|
||||
|
||||
"github.com/cenk/backoff"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@@ -1,9 +1,10 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"github.com/cenk/backoff"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cenk/backoff"
|
||||
)
|
||||
|
||||
func TestJobBackOff(t *testing.T) {
|
||||
|
@@ -1,8 +1,9 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"github.com/Sirupsen/logrus"
|
||||
"io"
|
||||
|
||||
"github.com/Sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
|
22
middlewares/addPrefix.go
Normal file
22
middlewares/addPrefix.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// AddPrefix is a middleware used to add prefix to an URL request
|
||||
type AddPrefix struct {
|
||||
Handler http.Handler
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (s *AddPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = s.Prefix + r.URL.Path
|
||||
r.RequestURI = r.URL.RequestURI()
|
||||
s.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// SetHandler sets handler
|
||||
func (s *AddPrefix) SetHandler(Handler http.Handler) {
|
||||
s.Handler = Handler
|
||||
}
|
@@ -2,12 +2,13 @@ package middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/abbot/go-http-auth"
|
||||
"github.com/codegangsta/negroni"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/types"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Authenticator is a middleware that provides HTTP basic and digest authentication
|
||||
@@ -31,9 +32,13 @@ func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) {
|
||||
basicAuth := auth.NewBasicAuthenticator("traefik", authenticator.secretBasic)
|
||||
authenticator.handler = negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
if username := basicAuth.CheckAuth(r); username == "" {
|
||||
log.Debugf("Auth failed...")
|
||||
log.Debugf("Basic auth failed...")
|
||||
basicAuth.RequireAuth(w, r)
|
||||
} else {
|
||||
log.Debugf("Basic auth success...")
|
||||
if authConfig.HeaderField != "" {
|
||||
r.Header[authConfig.HeaderField] = []string{username}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
@@ -45,8 +50,13 @@ func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) {
|
||||
digestAuth := auth.NewDigestAuthenticator("traefik", authenticator.secretDigest)
|
||||
authenticator.handler = negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
if username, _ := digestAuth.CheckAuth(r); username == "" {
|
||||
log.Debugf("Digest auth failed...")
|
||||
digestAuth.RequireAuth(w, r)
|
||||
} else {
|
||||
log.Debugf("Digest auth success...")
|
||||
if authConfig.HeaderField != "" {
|
||||
r.Header[authConfig.HeaderField] = []string{username}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
|
@@ -2,13 +2,14 @@ package middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/codegangsta/negroni"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/codegangsta/negroni"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBasicAuthFail(t *testing.T) {
|
||||
@@ -101,3 +102,33 @@ func TestDigestAuthFail(t *testing.T) {
|
||||
assert.NoError(t, err, "there should be no error")
|
||||
assert.Equal(t, http.StatusUnauthorized, res.StatusCode, "they should be equal")
|
||||
}
|
||||
|
||||
func TestBasicAuthUserHeader(t *testing.T) {
|
||||
authMiddleware, err := NewAuthenticator(&types.Auth{
|
||||
Basic: &types.Basic{
|
||||
Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"},
|
||||
},
|
||||
HeaderField: "X-WebAuth-User",
|
||||
})
|
||||
assert.NoError(t, err, "there should be no error")
|
||||
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "test", r.Header["X-WebAuth-User"][0], "auth user should be set")
|
||||
fmt.Fprintln(w, "traefik")
|
||||
})
|
||||
n := negroni.New(authMiddleware)
|
||||
n.UseHandler(handler)
|
||||
ts := httptest.NewServer(n)
|
||||
defer ts.Close()
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", ts.URL, nil)
|
||||
req.SetBasicAuth("test", "test")
|
||||
res, err := client.Do(req)
|
||||
assert.NoError(t, err, "there should be no error")
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode, "they should be equal")
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
assert.NoError(t, err, "there should be no error")
|
||||
assert.Equal(t, "traefik\n", string(body), "they should be equal")
|
||||
}
|
||||
|
@@ -1,8 +1,9 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/NYTimes/gziphandler"
|
||||
"net/http"
|
||||
|
||||
"github.com/NYTimes/gziphandler"
|
||||
)
|
||||
|
||||
// Compress is a middleware that allows redirections
|
||||
|
@@ -1,9 +1,10 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/containous/mux"
|
||||
"github.com/containous/traefik/safe"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// HandlerSwitcher allows hot switching of http.ServeMux
|
||||
|
@@ -2,8 +2,6 @@ package middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
shellwords "github.com/mattn/go-shellwords"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -11,6 +9,9 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
shellwords "github.com/mattn/go-shellwords"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type logtestResponseWriter struct{}
|
||||
|
51
middlewares/metrics.go
Normal file
51
middlewares/metrics.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Metrics is an Interface that must be satisfied by any system that
|
||||
// wants to expose and monitor metrics
|
||||
type Metrics interface {
|
||||
getReqsCounter() metrics.Counter
|
||||
getLatencyHistogram() metrics.Histogram
|
||||
handler() http.Handler
|
||||
}
|
||||
|
||||
// MetricsWrapper is a Negroni compatible Handler which relies on a
|
||||
// given Metrics implementation to expose and monitor Traefik metrics
|
||||
type MetricsWrapper struct {
|
||||
Impl Metrics
|
||||
}
|
||||
|
||||
// NewMetricsWrapper return a MetricsWrapper struct with
|
||||
// a given Metrics implementation e.g Prometheuss
|
||||
func NewMetricsWrapper(impl Metrics) *MetricsWrapper {
|
||||
var metricsWrapper = MetricsWrapper{
|
||||
Impl: impl,
|
||||
}
|
||||
|
||||
return &metricsWrapper
|
||||
}
|
||||
|
||||
func (m *MetricsWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
start := time.Now()
|
||||
prw := &responseRecorder{rw, http.StatusOK}
|
||||
next(prw, r)
|
||||
labels := []string{"code", strconv.Itoa(prw.StatusCode()), "method", r.Method}
|
||||
m.Impl.getReqsCounter().With(labels...).Add(1)
|
||||
m.Impl.getLatencyHistogram().Observe(float64(time.Since(start).Seconds()))
|
||||
}
|
||||
|
||||
func (rw *responseRecorder) StatusCode() int {
|
||||
return rw.statusCode
|
||||
}
|
||||
|
||||
// Handler is the chance for the Metrics implementation
|
||||
// to expose its metrics on a server endpoint
|
||||
func (m *MetricsWrapper) Handler() http.Handler {
|
||||
return m.Impl.handler()
|
||||
}
|
89
middlewares/prometheus.go
Normal file
89
middlewares/prometheus.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/go-kit/kit/metrics"
|
||||
"github.com/go-kit/kit/metrics/prometheus"
|
||||
stdprometheus "github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
reqsName = "traefik_requests_total"
|
||||
latencyName = "traefik_request_duration_seconds"
|
||||
)
|
||||
|
||||
// Prometheus is an Implementation for Metrics that exposes prometheus metrics for the latency
|
||||
// and the number of requests partitioned by status code and method.
|
||||
type Prometheus struct {
|
||||
reqsCounter metrics.Counter
|
||||
latencyHistogram metrics.Histogram
|
||||
}
|
||||
|
||||
func (p *Prometheus) getReqsCounter() metrics.Counter {
|
||||
return p.reqsCounter
|
||||
}
|
||||
|
||||
func (p *Prometheus) getLatencyHistogram() metrics.Histogram {
|
||||
return p.latencyHistogram
|
||||
}
|
||||
|
||||
// NewPrometheus returns a new prometheus Metrics implementation.
|
||||
func NewPrometheus(name string, config *types.Prometheus) *Prometheus {
|
||||
var m Prometheus
|
||||
|
||||
cv := stdprometheus.NewCounterVec(
|
||||
stdprometheus.CounterOpts{
|
||||
Name: reqsName,
|
||||
Help: "How many HTTP requests processed, partitioned by status code and method.",
|
||||
ConstLabels: stdprometheus.Labels{"service": name},
|
||||
},
|
||||
[]string{"code", "method"},
|
||||
)
|
||||
|
||||
err := stdprometheus.Register(cv)
|
||||
if err != nil {
|
||||
e, ok := err.(stdprometheus.AlreadyRegisteredError)
|
||||
if !ok {
|
||||
panic(err)
|
||||
}
|
||||
m.reqsCounter = prometheus.NewCounter(e.ExistingCollector.(*stdprometheus.CounterVec))
|
||||
} else {
|
||||
m.reqsCounter = prometheus.NewCounter(cv)
|
||||
}
|
||||
|
||||
var buckets []float64
|
||||
if config.Buckets != nil {
|
||||
buckets = config.Buckets
|
||||
} else {
|
||||
buckets = []float64{0.1, 0.3, 1.2, 5}
|
||||
}
|
||||
|
||||
hv := stdprometheus.NewHistogramVec(
|
||||
stdprometheus.HistogramOpts{
|
||||
Name: latencyName,
|
||||
Help: "How long it took to process the request.",
|
||||
ConstLabels: stdprometheus.Labels{"service": name},
|
||||
Buckets: buckets,
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
|
||||
err = stdprometheus.Register(hv)
|
||||
if err != nil {
|
||||
e, ok := err.(stdprometheus.AlreadyRegisteredError)
|
||||
if !ok {
|
||||
panic(err)
|
||||
}
|
||||
m.latencyHistogram = prometheus.NewHistogram(e.ExistingCollector.(*stdprometheus.HistogramVec))
|
||||
} else {
|
||||
m.latencyHistogram = prometheus.NewHistogram(hv)
|
||||
}
|
||||
|
||||
return &m
|
||||
}
|
||||
|
||||
func (p *Prometheus) handler() http.Handler {
|
||||
return promhttp.Handler()
|
||||
}
|
130
middlewares/prometheus_test.go
Normal file
130
middlewares/prometheus_test.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/codegangsta/negroni"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPrometheus(t *testing.T) {
|
||||
metricsFamily, err := prometheus.DefaultGatherer.Gather()
|
||||
if err != nil {
|
||||
t.Fatalf("could not gather metrics family: %s", err)
|
||||
}
|
||||
initialMetricsFamilyCount := len(metricsFamily)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
n := negroni.New()
|
||||
metricsMiddlewareBackend := NewMetricsWrapper(NewPrometheus("test", &types.Prometheus{}))
|
||||
n.Use(metricsMiddlewareBackend)
|
||||
r := http.NewServeMux()
|
||||
r.Handle("/metrics", promhttp.Handler())
|
||||
r.HandleFunc(`/ok`, func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintln(w, "ok")
|
||||
})
|
||||
n.UseHandler(r)
|
||||
|
||||
req1, err := http.NewRequest("GET", "http://localhost:3000/ok", nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
req2, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
n.ServeHTTP(recorder, req1)
|
||||
n.ServeHTTP(recorder, req2)
|
||||
body := recorder.Body.String()
|
||||
if !strings.Contains(body, reqsName) {
|
||||
t.Errorf("body does not contain request total entry '%s'", reqsName)
|
||||
}
|
||||
if !strings.Contains(body, latencyName) {
|
||||
t.Errorf("body does not contain request duration entry '%s'", latencyName)
|
||||
}
|
||||
|
||||
// Register the same metrics again
|
||||
metricsMiddlewareBackend = NewMetricsWrapper(NewPrometheus("test", &types.Prometheus{}))
|
||||
n = negroni.New()
|
||||
n.Use(metricsMiddlewareBackend)
|
||||
n.UseHandler(r)
|
||||
|
||||
n.ServeHTTP(recorder, req2)
|
||||
|
||||
metricsFamily, err = prometheus.DefaultGatherer.Gather()
|
||||
if err != nil {
|
||||
t.Fatalf("could not gather metrics family: %s", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
labels map[string]string
|
||||
assert func(*dto.MetricFamily)
|
||||
}{
|
||||
{
|
||||
name: reqsName,
|
||||
labels: map[string]string{
|
||||
"code": "200",
|
||||
"method": "GET",
|
||||
"service": "test",
|
||||
},
|
||||
assert: func(family *dto.MetricFamily) {
|
||||
cv := uint(family.Metric[0].Counter.GetValue())
|
||||
if cv != 3 {
|
||||
t.Errorf("gathered metrics do not contain correct value for total requests, got %d", cv)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: latencyName,
|
||||
labels: map[string]string{
|
||||
"service": "test",
|
||||
},
|
||||
assert: func(family *dto.MetricFamily) {
|
||||
sc := family.Metric[0].Histogram.GetSampleCount()
|
||||
if sc != 3 {
|
||||
t.Errorf("gathered metrics do not contain correct sample count for request duration, got %d", sc)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, len(tests), len(metricsFamily)-initialMetricsFamilyCount, "gathered traefic metrics count does not match tests count")
|
||||
|
||||
for _, test := range tests {
|
||||
family := findMetricFamily(test.name, metricsFamily)
|
||||
if family == nil {
|
||||
t.Errorf("gathered metrics do not contain '%s'", test.name)
|
||||
continue
|
||||
}
|
||||
for _, label := range family.Metric[0].Label {
|
||||
val, ok := test.labels[*label.Name]
|
||||
if !ok {
|
||||
t.Errorf("'%s' metric contains unexpected label '%s'", test.name, label)
|
||||
} else if val != *label.Value {
|
||||
t.Errorf("label '%s' in metric '%s' has wrong value '%s'", label, test.name, *label.Value)
|
||||
}
|
||||
}
|
||||
test.assert(family)
|
||||
}
|
||||
}
|
||||
|
||||
func findMetricFamily(name string, families []*dto.MetricFamily) *dto.MetricFamily {
|
||||
for _, family := range families {
|
||||
if family.GetName() == name {
|
||||
return family
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -3,10 +3,12 @@ package middlewares
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/vulcand/oxy/utils"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/vulcand/oxy/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -31,6 +33,13 @@ func NewRetry(attempts int, next http.Handler) *Retry {
|
||||
}
|
||||
|
||||
func (retry *Retry) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
|
||||
// if we might make multiple attempts, swap the body for an ioutil.NopCloser
|
||||
// cf https://github.com/containous/traefik/issues/1008
|
||||
if retry.attempts > 1 {
|
||||
body := r.Body
|
||||
defer body.Close()
|
||||
r.Body = ioutil.NopCloser(body)
|
||||
}
|
||||
attempts := 1
|
||||
for {
|
||||
recorder := NewRecorder()
|
||||
|
@@ -1,9 +1,10 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/vulcand/vulcand/plugin/rewrite"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Rewrite is a middleware that allows redirections
|
||||
|
92
middlewares/stats.go
Normal file
92
middlewares/stats.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package middlewares
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StatsRecorder is an optional middleware that records more details statistics
|
||||
// about requests and how they are processed. This currently consists of recent
|
||||
// requests that have caused errors (4xx and 5xx status codes), making it easy
|
||||
// to pinpoint problems.
|
||||
type StatsRecorder struct {
|
||||
mutex sync.RWMutex
|
||||
numRecentErrors int
|
||||
recentErrors []*statsError
|
||||
}
|
||||
|
||||
// NewStatsRecorder returns a new StatsRecorder
|
||||
func NewStatsRecorder(numRecentErrors int) *StatsRecorder {
|
||||
return &StatsRecorder{
|
||||
numRecentErrors: numRecentErrors,
|
||||
}
|
||||
}
|
||||
|
||||
// Stats includes all of the stats gathered by the recorder.
|
||||
type Stats struct {
|
||||
RecentErrors []*statsError `json:"recent_errors"`
|
||||
}
|
||||
|
||||
// statsError represents an error that has occurred during request processing.
|
||||
type statsError struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Status string `json:"status"`
|
||||
Method string `json:"method"`
|
||||
Host string `json:"host"`
|
||||
Path string `json:"path"`
|
||||
Time time.Time `json:"time"`
|
||||
}
|
||||
|
||||
// responseRecorder captures information from the response and preserves it for
|
||||
// later analysis.
|
||||
type responseRecorder struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
// WriteHeader captures the status code for later retrieval.
|
||||
func (r *responseRecorder) WriteHeader(status int) {
|
||||
r.ResponseWriter.WriteHeader(status)
|
||||
r.statusCode = status
|
||||
}
|
||||
|
||||
// ServeHTTP silently extracts information from the request and response as it
|
||||
// is processed. If the response is 4xx or 5xx, add it to the list of 10 most
|
||||
// recent errors.
|
||||
func (s *StatsRecorder) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
recorder := &responseRecorder{w, http.StatusOK}
|
||||
next(recorder, r)
|
||||
if recorder.statusCode >= 400 {
|
||||
s.mutex.Lock()
|
||||
defer s.mutex.Unlock()
|
||||
s.recentErrors = append([]*statsError{
|
||||
{
|
||||
StatusCode: recorder.statusCode,
|
||||
Status: http.StatusText(recorder.statusCode),
|
||||
Method: r.Method,
|
||||
Host: r.Host,
|
||||
Path: r.URL.Path,
|
||||
Time: time.Now(),
|
||||
},
|
||||
}, s.recentErrors...)
|
||||
// Limit the size of the list to numRecentErrors
|
||||
if len(s.recentErrors) > s.numRecentErrors {
|
||||
s.recentErrors = s.recentErrors[:s.numRecentErrors]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Data returns a copy of the statistics that have been gathered.
|
||||
func (s *StatsRecorder) Data() *Stats {
|
||||
s.mutex.RLock()
|
||||
defer s.mutex.RUnlock()
|
||||
|
||||
// We can't return the slice directly or a race condition might develop
|
||||
recentErrors := make([]*statsError, len(s.recentErrors))
|
||||
copy(recentErrors, s.recentErrors)
|
||||
|
||||
return &Stats{
|
||||
RecentErrors: recentErrors,
|
||||
}
|
||||
}
|
@@ -53,3 +53,4 @@ pages:
|
||||
- 'Kubernetes': 'user-guide/kubernetes.md'
|
||||
- 'Key-value store configuration': 'user-guide/kv-config.md'
|
||||
- 'Clustering/HA': 'user-guide/cluster.md'
|
||||
- Benchmarks: benchmarks.md
|
||||
|
@@ -147,7 +147,7 @@ func (_m *Marathon) CreateApplication(application *marathon.Application) (*marat
|
||||
}
|
||||
|
||||
// DeleteApplication provides a mock function with given fields: name
|
||||
func (_m *Marathon) DeleteApplication(name string) (*marathon.DeploymentID, error) {
|
||||
func (_m *Marathon) DeleteApplication(name string, force bool) (*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *marathon.DeploymentID
|
||||
@@ -534,7 +534,7 @@ func (_m *Marathon) CreateGroup(group *marathon.Group) error {
|
||||
}
|
||||
|
||||
// DeleteGroup provides a mock function with given fields: name
|
||||
func (_m *Marathon) DeleteGroup(name string) (*marathon.DeploymentID, error) {
|
||||
func (_m *Marathon) DeleteGroup(name string, force bool) (*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(name)
|
||||
|
||||
var r0 *marathon.DeploymentID
|
||||
@@ -557,7 +557,7 @@ func (_m *Marathon) DeleteGroup(name string) (*marathon.DeploymentID, error) {
|
||||
}
|
||||
|
||||
// UpdateGroup provides a mock function with given fields: id, group
|
||||
func (_m *Marathon) UpdateGroup(id string, group *marathon.Group) (*marathon.DeploymentID, error) {
|
||||
func (_m *Marathon) UpdateGroup(id string, group *marathon.Group, force bool) (*marathon.DeploymentID, error) {
|
||||
ret := _m.Called(id, group)
|
||||
|
||||
var r0 *marathon.DeploymentID
|
||||
@@ -719,17 +719,18 @@ func (_m *Marathon) Subscriptions() (*marathon.Subscriptions, error) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
func (_m *Marathon) AddEventsListener(filter int) (marathon.EventsChannel, error) {
|
||||
update := make(marathon.EventsChannel, 5)
|
||||
ret := _m.Called(update, filter)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(marathon.EventsChannel, int) error); ok {
|
||||
r0 = rf(channel, filter)
|
||||
r0 = rf(update, filter)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
return update, r0
|
||||
}
|
||||
|
||||
// RemoveEventsListener provides a mock function with given fields: channel
|
||||
@@ -850,3 +851,33 @@ func (_m *Marathon) AbdicateLeader() (string, error) {
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// ApplicationBy mocks the marathon client function but does nothing
|
||||
func (_m *Marathon) ApplicationBy(name string, opts *marathon.GetAppOpts) (*marathon.Application, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Subscribe mocks the marathon client function but does nothing
|
||||
func (_m *Marathon) Subscribe(string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Queue mocks the marathon client function but does nothing
|
||||
func (_m *Marathon) Queue() (*marathon.Queue, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DeleteQueueDelay mocks the marathon client function but does nothing
|
||||
func (_m *Marathon) DeleteQueueDelay(appID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupsBy mocks the marathon client function but does nothing
|
||||
func (_m *Marathon) GroupsBy(opts *marathon.GetGroupOpts) (*marathon.Groups, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GroupBy mocks the marathon client function but does nothing
|
||||
func (_m *Marathon) GroupBy(name string, opts *marathon.GetGroupOpts) (*marathon.Group, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
|
@@ -2,6 +2,7 @@ package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
|
@@ -334,7 +334,7 @@ func (provider *ConsulCatalog) Provide(configurationChan chan<- types.ConfigMess
|
||||
operation := func() error {
|
||||
return provider.watch(configurationChan, stop)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot connect to consul server %+v", err)
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math"
|
||||
"net"
|
||||
@@ -10,8 +11,6 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
"github.com/cenk/backoff"
|
||||
"github.com/containous/traefik/job"
|
||||
@@ -32,8 +31,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// DockerAPIVersion is a constant holding the version of the Docker API traefik will use
|
||||
DockerAPIVersion string = "1.21"
|
||||
// SwarmAPIVersion is a constant holding the version of the Docker API traefik will use
|
||||
SwarmAPIVersion string = "1.24"
|
||||
// SwarmDefaultWatchTime is the duration of the interval when polling docker
|
||||
@@ -55,6 +52,7 @@ type Docker struct {
|
||||
|
||||
// dockerData holds the need data to the Docker provider
|
||||
type dockerData struct {
|
||||
ServiceName string
|
||||
Name string
|
||||
Labels map[string]string // List of labels set to container or service
|
||||
NetworkSettings networkSettings
|
||||
@@ -132,7 +130,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po
|
||||
log.Debugf("Docker connection established with docker %s (API %s)", version.Version, version.APIVersion)
|
||||
var dockerDataList []dockerData
|
||||
if provider.SwarmMode {
|
||||
dockerDataList, err = listServices(ctx, dockerClient)
|
||||
dockerDataList, err = provider.listServices(ctx, dockerClient)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to list services for docker swarm mode, error %s", err)
|
||||
return err
|
||||
@@ -159,7 +157,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
services, err := listServices(ctx, dockerClient)
|
||||
services, err := provider.listServices(ctx, dockerClient)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to list services for docker, error %s", err)
|
||||
return
|
||||
@@ -230,7 +228,7 @@ func (provider *Docker) Provide(configurationChan chan<- types.ConfigMessage, po
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Docker connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot connect to docker server %+v", err)
|
||||
}
|
||||
@@ -259,9 +257,8 @@ func (provider *Docker) loadDockerConfig(containersInspected []dockerData) *type
|
||||
"getMaxConnAmount": provider.getMaxConnAmount,
|
||||
"getMaxConnExtractorFunc": provider.getMaxConnExtractorFunc,
|
||||
"getSticky": provider.getSticky,
|
||||
"replace": replace,
|
||||
"getIsBackendLBSwarm": provider.getIsBackendLBSwarm,
|
||||
}
|
||||
|
||||
// filter containers
|
||||
filteredContainers := fun.Filter(func(container dockerData) bool {
|
||||
return provider.containerFilter(container)
|
||||
@@ -383,6 +380,11 @@ func (provider *Docker) containerFilter(container dockerData) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(provider.getFrontendRule(container)) == 0 {
|
||||
log.Debugf("Filtering container with empty frontend rule %s", container.Name)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -397,14 +399,17 @@ func (provider *Docker) getFrontendRule(container dockerData) string {
|
||||
if label, err := getLabel(container, "traefik.frontend.rule"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "Host:" + provider.getSubDomain(container.Name) + "." + provider.Domain
|
||||
if len(provider.Domain) > 0 {
|
||||
return "Host:" + provider.getSubDomain(container.ServiceName) + "." + provider.Domain
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (provider *Docker) getBackend(container dockerData) string {
|
||||
if label, err := getLabel(container, "traefik.backend"); err == nil {
|
||||
return normalize(label)
|
||||
}
|
||||
return normalize(container.Name)
|
||||
return normalize(container.ServiceName)
|
||||
}
|
||||
|
||||
func (provider *Docker) getIPAddress(container dockerData) string {
|
||||
@@ -415,6 +420,8 @@ func (provider *Docker) getIPAddress(container dockerData) string {
|
||||
if network != nil {
|
||||
return network.Addr
|
||||
}
|
||||
|
||||
log.Warnf("Could not find network named '%s' for container '%s'! Maybe you're missing the project's prefix in the label? Defaulting to first available network.", label, container.Name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,8 +466,15 @@ func (provider *Docker) getWeight(container dockerData) string {
|
||||
}
|
||||
|
||||
func (provider *Docker) getSticky(container dockerData) string {
|
||||
if _, err := getLabel(container, "traefik.backend.loadbalancer.sticky"); err == nil {
|
||||
return "true"
|
||||
if label, err := getLabel(container, "traefik.backend.loadbalancer.sticky"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func (provider *Docker) getIsBackendLBSwarm(container dockerData) string {
|
||||
if label, err := getLabel(container, "traefik.backend.loadbalancer.swarm"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
@@ -556,6 +570,7 @@ func parseContainer(container dockertypes.ContainerJSON) dockerData {
|
||||
|
||||
if container.ContainerJSONBase != nil {
|
||||
dockerData.Name = container.ContainerJSONBase.Name
|
||||
dockerData.ServiceName = dockerData.Name //Default ServiceName to be the container's Name.
|
||||
|
||||
if container.ContainerJSONBase.HostConfig != nil {
|
||||
dockerData.NetworkSettings.NetworkMode = container.ContainerJSONBase.HostConfig.NetworkMode
|
||||
@@ -590,12 +605,12 @@ func parseContainer(container dockertypes.ContainerJSON) dockerData {
|
||||
return dockerData
|
||||
}
|
||||
|
||||
// Escape beginning slash "/", convert all others to dash "-"
|
||||
// Escape beginning slash "/", convert all others to dash "-", and convert underscores "_" to dash "-"
|
||||
func (provider *Docker) getSubDomain(name string) string {
|
||||
return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1)
|
||||
return strings.Replace(strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1), "_", "-", -1)
|
||||
}
|
||||
|
||||
func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) {
|
||||
func (provider *Docker) listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerData, error) {
|
||||
serviceList, err := dockerClient.ServiceList(ctx, dockertypes.ServiceListOptions{})
|
||||
if err != nil {
|
||||
return []dockerData{}, err
|
||||
@@ -616,11 +631,22 @@ func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerD
|
||||
}
|
||||
|
||||
var dockerDataList []dockerData
|
||||
var dockerDataListTasks []dockerData
|
||||
|
||||
for _, service := range serviceList {
|
||||
dockerData := parseService(service, networkMap)
|
||||
useSwarmLB, _ := strconv.ParseBool(provider.getIsBackendLBSwarm(dockerData))
|
||||
isGlobalSvc := service.Spec.Mode.Global != nil
|
||||
|
||||
dockerDataList = append(dockerDataList, dockerData)
|
||||
if useSwarmLB {
|
||||
dockerDataList = append(dockerDataList, dockerData)
|
||||
} else {
|
||||
dockerDataListTasks, err = listTasks(ctx, dockerClient, service.ID, dockerData, networkMap, isGlobalSvc)
|
||||
|
||||
for _, dockerDataTask := range dockerDataListTasks {
|
||||
dockerDataList = append(dockerDataList, dockerDataTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dockerDataList, err
|
||||
|
||||
@@ -628,6 +654,7 @@ func listServices(ctx context.Context, dockerClient client.APIClient) ([]dockerD
|
||||
|
||||
func parseService(service swarmtypes.Service, networkMap map[string]*dockertypes.NetworkResource) dockerData {
|
||||
dockerData := dockerData{
|
||||
ServiceName: service.Spec.Annotations.Name,
|
||||
Name: service.Spec.Annotations.Name,
|
||||
Labels: service.Spec.Annotations.Labels,
|
||||
NetworkSettings: networkSettings{},
|
||||
@@ -652,7 +679,60 @@ func parseService(service swarmtypes.Service, networkMap map[string]*dockertypes
|
||||
} else {
|
||||
log.Debug("Network not found, id: %s", virtualIP.NetworkID)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return dockerData
|
||||
}
|
||||
|
||||
func listTasks(ctx context.Context, dockerClient client.APIClient, serviceID string,
|
||||
serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool) ([]dockerData, error) {
|
||||
serviceIDFilter := filters.NewArgs()
|
||||
serviceIDFilter.Add("service", serviceID)
|
||||
serviceIDFilter.Add("desired-state", "running")
|
||||
taskList, err := dockerClient.TaskList(ctx, dockertypes.TaskListOptions{Filter: serviceIDFilter})
|
||||
|
||||
if err != nil {
|
||||
return []dockerData{}, err
|
||||
}
|
||||
var dockerDataList []dockerData
|
||||
|
||||
for _, task := range taskList {
|
||||
if task.Status.State != swarm.TaskStateRunning {
|
||||
continue
|
||||
}
|
||||
dockerData := parseTasks(task, serviceDockerData, networkMap, isGlobalSvc)
|
||||
dockerDataList = append(dockerDataList, dockerData)
|
||||
}
|
||||
return dockerDataList, err
|
||||
}
|
||||
|
||||
func parseTasks(task swarmtypes.Task, serviceDockerData dockerData, networkMap map[string]*dockertypes.NetworkResource, isGlobalSvc bool) dockerData {
|
||||
dockerData := dockerData{
|
||||
ServiceName: serviceDockerData.Name,
|
||||
Name: serviceDockerData.Name + "." + strconv.Itoa(task.Slot),
|
||||
Labels: serviceDockerData.Labels,
|
||||
NetworkSettings: networkSettings{},
|
||||
}
|
||||
|
||||
if isGlobalSvc == true {
|
||||
dockerData.Name = serviceDockerData.Name + "." + task.ID
|
||||
}
|
||||
|
||||
if task.NetworksAttachments != nil {
|
||||
dockerData.NetworkSettings.Networks = make(map[string]*networkData)
|
||||
for _, virtualIP := range task.NetworksAttachments {
|
||||
if networkService, present := networkMap[virtualIP.Network.ID]; present {
|
||||
// Not sure about this next loop - when would a task have multiple IP's for the same network?
|
||||
for _, addr := range virtualIP.Addresses {
|
||||
ip, _, _ := net.ParseCIDR(addr)
|
||||
network := &networkData{
|
||||
ID: virtualIP.Network.ID,
|
||||
Name: networkService.Name,
|
||||
Addr: ip.String(),
|
||||
}
|
||||
dockerData.NetworkSettings.Networks[network.Name] = network
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
8
provider/docker_unix.go
Normal file
8
provider/docker_unix.go
Normal file
@@ -0,0 +1,8 @@
|
||||
// +build !windows
|
||||
|
||||
package provider
|
||||
|
||||
const (
|
||||
// DockerAPIVersion is a constant holding the version of the Docker API traefik will use
|
||||
DockerAPIVersion string = "1.21"
|
||||
)
|
6
provider/docker_windows.go
Normal file
6
provider/docker_windows.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package provider
|
||||
|
||||
const (
|
||||
// DockerAPIVersion is a constant holding the version of the Docker API traefik will use
|
||||
DockerAPIVersion string = "1.24"
|
||||
)
|
473
provider/ecs.go
Normal file
473
provider/ecs.go
Normal file
@@ -0,0 +1,473 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/defaults"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/ecs"
|
||||
"github.com/cenk/backoff"
|
||||
"github.com/containous/traefik/job"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
)
|
||||
|
||||
var _ Provider = (*ECS)(nil)
|
||||
|
||||
// ECS holds configurations of the ECS provider.
|
||||
type ECS struct {
|
||||
BaseProvider `mapstructure:",squash"`
|
||||
|
||||
Domain string `description:"Default domain used"`
|
||||
ExposedByDefault bool `description:"Expose containers by default"`
|
||||
RefreshSeconds int `description:"Polling interval (in seconds)"`
|
||||
|
||||
// ECS lookup parameters
|
||||
Cluster string `description:"ECS Cluster Name"`
|
||||
Region string `description:"The AWS region to use for requests"`
|
||||
AccessKeyID string `description:"The AWS credentials access key to use for making requests"`
|
||||
SecretAccessKey string `description:"The AWS credentials access key to use for making requests"`
|
||||
}
|
||||
|
||||
type ecsInstance struct {
|
||||
Name string
|
||||
ID string
|
||||
task *ecs.Task
|
||||
taskDefinition *ecs.TaskDefinition
|
||||
container *ecs.Container
|
||||
containerDefinition *ecs.ContainerDefinition
|
||||
machine *ec2.Instance
|
||||
}
|
||||
|
||||
type awsClient struct {
|
||||
ecs *ecs.ECS
|
||||
ec2 *ec2.EC2
|
||||
}
|
||||
|
||||
func (provider *ECS) createClient() (*awsClient, error) {
|
||||
sess := session.New()
|
||||
ec2meta := ec2metadata.New(sess)
|
||||
if provider.Region == "" {
|
||||
log.Infoln("No EC2 region provided, querying instance metadata endpoint...")
|
||||
identity, err := ec2meta.GetInstanceIdentityDocument()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
provider.Region = identity.Region
|
||||
}
|
||||
|
||||
cfg := &aws.Config{
|
||||
Region: &provider.Region,
|
||||
Credentials: credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.StaticProvider{
|
||||
Value: credentials.Value{
|
||||
AccessKeyID: provider.AccessKeyID,
|
||||
SecretAccessKey: provider.SecretAccessKey,
|
||||
},
|
||||
},
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{},
|
||||
defaults.RemoteCredProvider(*(defaults.Config()), defaults.Handlers()),
|
||||
}),
|
||||
}
|
||||
|
||||
return &awsClient{
|
||||
ecs.New(sess, cfg),
|
||||
ec2.New(sess, cfg),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *ECS) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
|
||||
|
||||
provider.Constraints = append(provider.Constraints, constraints...)
|
||||
|
||||
handleCanceled := func(ctx context.Context, err error) error {
|
||||
if ctx.Err() == context.Canceled || err == context.Canceled {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
pool.Go(func(stop chan bool) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
select {
|
||||
case <-stop:
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
|
||||
operation := func() error {
|
||||
aws, err := provider.createClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configuration, err := provider.loadECSConfig(ctx, aws)
|
||||
if err != nil {
|
||||
return handleCanceled(ctx, err)
|
||||
}
|
||||
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "ecs",
|
||||
Configuration: configuration,
|
||||
}
|
||||
|
||||
if provider.Watch {
|
||||
reload := time.NewTicker(time.Second * time.Duration(provider.RefreshSeconds))
|
||||
defer reload.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-reload.C:
|
||||
configuration, err := provider.loadECSConfig(ctx, aws)
|
||||
if err != nil {
|
||||
return handleCanceled(ctx, err)
|
||||
}
|
||||
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "ecs",
|
||||
Configuration: configuration,
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return handleCanceled(ctx, ctx.Err())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("ECS connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot connect to ECS api %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func wrapAws(ctx context.Context, req *request.Request) error {
|
||||
req.HTTPRequest = req.HTTPRequest.WithContext(ctx)
|
||||
return req.Send()
|
||||
}
|
||||
|
||||
func (provider *ECS) loadECSConfig(ctx context.Context, client *awsClient) (*types.Configuration, error) {
|
||||
var ecsFuncMap = template.FuncMap{
|
||||
"filterFrontends": provider.filterFrontends,
|
||||
"getFrontendRule": provider.getFrontendRule,
|
||||
}
|
||||
|
||||
instances, err := provider.listInstances(ctx, client)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instances = fun.Filter(provider.filterInstance, instances).([]ecsInstance)
|
||||
|
||||
return provider.getConfiguration("templates/ecs.tmpl", ecsFuncMap, struct {
|
||||
Instances []ecsInstance
|
||||
}{
|
||||
instances,
|
||||
})
|
||||
}
|
||||
|
||||
// Find all running ECS tasks in a cluster, also collect the task definitions (for docker labels)
|
||||
// and the EC2 instance data
|
||||
func (provider *ECS) listInstances(ctx context.Context, client *awsClient) ([]ecsInstance, error) {
|
||||
var taskArns []*string
|
||||
req, _ := client.ecs.ListTasksRequest(&ecs.ListTasksInput{
|
||||
Cluster: &provider.Cluster,
|
||||
DesiredStatus: aws.String(ecs.DesiredStatusRunning),
|
||||
})
|
||||
|
||||
for ; req != nil; req = req.NextPage() {
|
||||
if err := wrapAws(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskArns = append(taskArns, req.Data.(*ecs.ListTasksOutput).TaskArns...)
|
||||
}
|
||||
|
||||
// Early return: if we can't list tasks we have nothing to
|
||||
// describe below - likely empty cluster/permissions are bad. This
|
||||
// stops the AWS API from returning a 401 when you DescribeTasks
|
||||
// with no input.
|
||||
if len(taskArns) == 0 {
|
||||
return []ecsInstance{}, nil
|
||||
}
|
||||
|
||||
chunkedTaskArns := provider.chunkedTaskArns(taskArns)
|
||||
var tasks []*ecs.Task
|
||||
|
||||
for _, arns := range chunkedTaskArns {
|
||||
req, taskResp := client.ecs.DescribeTasksRequest(&ecs.DescribeTasksInput{
|
||||
Tasks: arns,
|
||||
Cluster: &provider.Cluster,
|
||||
})
|
||||
|
||||
if err := wrapAws(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tasks = append(tasks, taskResp.Tasks...)
|
||||
|
||||
}
|
||||
|
||||
containerInstanceArns := make([]*string, 0)
|
||||
byContainerInstance := make(map[string]int)
|
||||
|
||||
taskDefinitionArns := make([]*string, 0)
|
||||
byTaskDefinition := make(map[string]int)
|
||||
|
||||
for _, task := range tasks {
|
||||
if _, found := byContainerInstance[*task.ContainerInstanceArn]; !found {
|
||||
byContainerInstance[*task.ContainerInstanceArn] = len(containerInstanceArns)
|
||||
containerInstanceArns = append(containerInstanceArns, task.ContainerInstanceArn)
|
||||
}
|
||||
if _, found := byTaskDefinition[*task.TaskDefinitionArn]; !found {
|
||||
byTaskDefinition[*task.TaskDefinitionArn] = len(taskDefinitionArns)
|
||||
taskDefinitionArns = append(taskDefinitionArns, task.TaskDefinitionArn)
|
||||
}
|
||||
}
|
||||
|
||||
machines, err := provider.lookupEc2Instances(ctx, client, containerInstanceArns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskDefinitions, err := provider.lookupTaskDefinitions(ctx, client, taskDefinitionArns)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var instances []ecsInstance
|
||||
for _, task := range tasks {
|
||||
|
||||
machineIdx := byContainerInstance[*task.ContainerInstanceArn]
|
||||
taskDefIdx := byTaskDefinition[*task.TaskDefinitionArn]
|
||||
|
||||
for _, container := range task.Containers {
|
||||
|
||||
taskDefinition := taskDefinitions[taskDefIdx]
|
||||
var containerDefinition *ecs.ContainerDefinition
|
||||
for _, def := range taskDefinition.ContainerDefinitions {
|
||||
if *container.Name == *def.Name {
|
||||
containerDefinition = def
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
instances = append(instances, ecsInstance{
|
||||
fmt.Sprintf("%s-%s", strings.Replace(*task.Group, ":", "-", 1), *container.Name),
|
||||
(*task.TaskArn)[len(*task.TaskArn)-12:],
|
||||
task,
|
||||
taskDefinition,
|
||||
container,
|
||||
containerDefinition,
|
||||
machines[machineIdx],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
func (provider *ECS) lookupEc2Instances(ctx context.Context, client *awsClient, containerArns []*string) ([]*ec2.Instance, error) {
|
||||
|
||||
order := make(map[string]int)
|
||||
instanceIds := make([]*string, len(containerArns))
|
||||
instances := make([]*ec2.Instance, len(containerArns))
|
||||
for i, arn := range containerArns {
|
||||
order[*arn] = i
|
||||
}
|
||||
|
||||
req, _ := client.ecs.DescribeContainerInstancesRequest(&ecs.DescribeContainerInstancesInput{
|
||||
ContainerInstances: containerArns,
|
||||
Cluster: &provider.Cluster,
|
||||
})
|
||||
|
||||
for ; req != nil; req = req.NextPage() {
|
||||
if err := wrapAws(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
containerResp := req.Data.(*ecs.DescribeContainerInstancesOutput)
|
||||
for i, container := range containerResp.ContainerInstances {
|
||||
order[*container.Ec2InstanceId] = order[*container.ContainerInstanceArn]
|
||||
instanceIds[i] = container.Ec2InstanceId
|
||||
}
|
||||
}
|
||||
|
||||
req, _ = client.ec2.DescribeInstancesRequest(&ec2.DescribeInstancesInput{
|
||||
InstanceIds: instanceIds,
|
||||
})
|
||||
|
||||
for ; req != nil; req = req.NextPage() {
|
||||
if err := wrapAws(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
instancesResp := req.Data.(*ec2.DescribeInstancesOutput)
|
||||
for _, r := range instancesResp.Reservations {
|
||||
for _, i := range r.Instances {
|
||||
if i.InstanceId != nil {
|
||||
instances[order[*i.InstanceId]] = i
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
func (provider *ECS) lookupTaskDefinitions(ctx context.Context, client *awsClient, taskDefArns []*string) ([]*ecs.TaskDefinition, error) {
|
||||
taskDefinitions := make([]*ecs.TaskDefinition, len(taskDefArns))
|
||||
for i, arn := range taskDefArns {
|
||||
|
||||
req, resp := client.ecs.DescribeTaskDefinitionRequest(&ecs.DescribeTaskDefinitionInput{
|
||||
TaskDefinition: arn,
|
||||
})
|
||||
|
||||
if err := wrapAws(ctx, req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
taskDefinitions[i] = resp.TaskDefinition
|
||||
}
|
||||
return taskDefinitions, nil
|
||||
}
|
||||
|
||||
func (i ecsInstance) label(k string) string {
|
||||
if v, found := i.containerDefinition.DockerLabels[k]; found {
|
||||
return *v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (provider *ECS) filterInstance(i ecsInstance) bool {
|
||||
if len(i.container.NetworkBindings) == 0 {
|
||||
log.Debugf("Filtering ecs instance without port %s (%s)", i.Name, i.ID)
|
||||
return false
|
||||
}
|
||||
|
||||
if i.machine == nil ||
|
||||
i.machine.State == nil ||
|
||||
i.machine.State.Name == nil {
|
||||
log.Debugf("Filtering ecs instance in an missing ec2 information %s (%s)", i.Name, i.ID)
|
||||
return false
|
||||
}
|
||||
|
||||
if *i.machine.State.Name != ec2.InstanceStateNameRunning {
|
||||
log.Debugf("Filtering ecs instance in an incorrect state %s (%s) (state = %s)", i.Name, i.ID, *i.machine.State.Name)
|
||||
return false
|
||||
}
|
||||
|
||||
if i.machine.PrivateIpAddress == nil {
|
||||
log.Debugf("Filtering ecs instance without an ip address %s (%s)", i.Name, i.ID)
|
||||
return false
|
||||
}
|
||||
|
||||
label := i.label("traefik.enable")
|
||||
enabled := provider.ExposedByDefault && label != "false" || label == "true"
|
||||
if !enabled {
|
||||
log.Debugf("Filtering disabled ecs instance %s (%s) (traefik.enabled = '%s')", i.Name, i.ID, label)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (provider *ECS) filterFrontends(instances []ecsInstance) []ecsInstance {
|
||||
byName := make(map[string]bool)
|
||||
|
||||
return fun.Filter(func(i ecsInstance) bool {
|
||||
if _, found := byName[i.Name]; !found {
|
||||
byName[i.Name] = true
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}, instances).([]ecsInstance)
|
||||
}
|
||||
|
||||
func (provider *ECS) getFrontendRule(i ecsInstance) string {
|
||||
if label := i.label("traefik.frontend.rule"); label != "" {
|
||||
return label
|
||||
}
|
||||
return "Host:" + strings.ToLower(strings.Replace(i.Name, "_", "-", -1)) + "." + provider.Domain
|
||||
}
|
||||
|
||||
// ECS expects no more than 100 parameters be passed to a DescribeTask call; thus, pack
|
||||
// each string into an array capped at 100 elements
|
||||
func (provider *ECS) chunkedTaskArns(tasks []*string) [][]*string {
|
||||
var chunkedTasks [][]*string
|
||||
for i := 0; i < len(tasks); i += 100 {
|
||||
sliceEnd := -1
|
||||
if i+100 < len(tasks) {
|
||||
sliceEnd = i + 100
|
||||
} else {
|
||||
sliceEnd = len(tasks)
|
||||
}
|
||||
chunkedTasks = append(chunkedTasks, tasks[i:sliceEnd])
|
||||
}
|
||||
return chunkedTasks
|
||||
}
|
||||
|
||||
func (i ecsInstance) Protocol() string {
|
||||
if label := i.label("traefik.protocol"); label != "" {
|
||||
return label
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (i ecsInstance) Host() string {
|
||||
return *i.machine.PrivateIpAddress
|
||||
}
|
||||
|
||||
func (i ecsInstance) Port() string {
|
||||
return strconv.FormatInt(*i.container.NetworkBindings[0].HostPort, 10)
|
||||
}
|
||||
|
||||
func (i ecsInstance) Weight() string {
|
||||
if label := i.label("traefik.weight"); label != "" {
|
||||
return label
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func (i ecsInstance) PassHostHeader() string {
|
||||
if label := i.label("traefik.frontend.passHostHeader"); label != "" {
|
||||
return label
|
||||
}
|
||||
return "true"
|
||||
}
|
||||
|
||||
func (i ecsInstance) Priority() string {
|
||||
if label := i.label("traefik.frontend.priority"); label != "" {
|
||||
return label
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func (i ecsInstance) EntryPoints() []string {
|
||||
if label := i.label("traefik.frontend.entryPoints"); label != "" {
|
||||
return strings.Split(label, ",")
|
||||
}
|
||||
return []string{}
|
||||
}
|
349
provider/ecs_test.go
Normal file
349
provider/ecs_test.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/ecs"
|
||||
)
|
||||
|
||||
func makeEcsInstance(containerDef *ecs.ContainerDefinition) ecsInstance {
|
||||
container := &ecs.Container{
|
||||
Name: containerDef.Name,
|
||||
NetworkBindings: make([]*ecs.NetworkBinding, len(containerDef.PortMappings)),
|
||||
}
|
||||
|
||||
for i, pm := range containerDef.PortMappings {
|
||||
container.NetworkBindings[i] = &ecs.NetworkBinding{
|
||||
HostPort: pm.HostPort,
|
||||
ContainerPort: pm.ContainerPort,
|
||||
Protocol: pm.Protocol,
|
||||
BindIP: aws.String("0.0.0.0"),
|
||||
}
|
||||
}
|
||||
|
||||
return ecsInstance{
|
||||
Name: "foo-http",
|
||||
ID: "123456789abc",
|
||||
task: &ecs.Task{
|
||||
Containers: []*ecs.Container{container},
|
||||
},
|
||||
taskDefinition: &ecs.TaskDefinition{
|
||||
ContainerDefinitions: []*ecs.ContainerDefinition{containerDef},
|
||||
},
|
||||
container: container,
|
||||
containerDefinition: containerDef,
|
||||
machine: &ec2.Instance{
|
||||
PrivateIpAddress: aws.String("10.0.0.0"),
|
||||
State: &ec2.InstanceState{
|
||||
Name: aws.String(ec2.InstanceStateNameRunning),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func simpleEcsInstance(labels map[string]*string) ecsInstance {
|
||||
return makeEcsInstance(&ecs.ContainerDefinition{
|
||||
Name: aws.String("http"),
|
||||
PortMappings: []*ecs.PortMapping{{
|
||||
HostPort: aws.Int64(80),
|
||||
ContainerPort: aws.Int64(80),
|
||||
Protocol: aws.String("tcp"),
|
||||
}},
|
||||
DockerLabels: labels,
|
||||
})
|
||||
}
|
||||
|
||||
func TestEcsProtocol(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "http",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: "https",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.protocol": aws.String("https"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
value := c.instanceInfo.Protocol()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %v, got %v (case %d)", c.expected, value, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsHost(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "10.0.0.0",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
value := c.instanceInfo.Host()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %v, got %v (case %d)", c.expected, value, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsPort(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "80",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
value := c.instanceInfo.Port()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %v, got %v (case %d)", c.expected, value, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsWeight(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "0",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: "10",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.weight": aws.String("10"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
value := c.instanceInfo.Weight()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %v, got %v (case %d)", c.expected, value, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsPassHostHeader(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "true",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: "false",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.frontend.passHostHeader": aws.String("false"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
value := c.instanceInfo.PassHostHeader()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %v, got %v (case %d)", c.expected, value, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsPriority(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: "0",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: "10",
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.frontend.priority": aws.String("10"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
value := c.instanceInfo.Priority()
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %v, got %v (case %d)", c.expected, value, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEcsEntryPoints(t *testing.T) {
|
||||
cases := []struct {
|
||||
expected []string
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: []string{},
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: []string{"http"},
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.frontend.entryPoints": aws.String("http"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
expected: []string{"http", "https"},
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.frontend.entryPoints": aws.String("http,https"),
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
value := c.instanceInfo.EntryPoints()
|
||||
if !reflect.DeepEqual(value, c.expected) {
|
||||
t.Fatalf("Should have been %v, got %v (case %d)", c.expected, value, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterInstance(t *testing.T) {
|
||||
|
||||
nilPrivateIP := simpleEcsInstance(map[string]*string{})
|
||||
nilPrivateIP.machine.PrivateIpAddress = nil
|
||||
|
||||
nilMachine := simpleEcsInstance(map[string]*string{})
|
||||
nilMachine.machine = nil
|
||||
|
||||
nilMachineState := simpleEcsInstance(map[string]*string{})
|
||||
nilMachineState.machine.State = nil
|
||||
|
||||
nilMachineStateName := simpleEcsInstance(map[string]*string{})
|
||||
nilMachineStateName.machine.State.Name = nil
|
||||
|
||||
invalidMachineState := simpleEcsInstance(map[string]*string{})
|
||||
invalidMachineState.machine.State.Name = aws.String(ec2.InstanceStateNameStopped)
|
||||
|
||||
cases := []struct {
|
||||
expected bool
|
||||
exposedByDefault bool
|
||||
instanceInfo ecsInstance
|
||||
}{
|
||||
{
|
||||
expected: true,
|
||||
exposedByDefault: true,
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: false,
|
||||
exposedByDefault: false,
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{}),
|
||||
},
|
||||
{
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.enable": aws.String("false"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
expected: true,
|
||||
exposedByDefault: false,
|
||||
instanceInfo: simpleEcsInstance(map[string]*string{
|
||||
"traefik.enable": aws.String("true"),
|
||||
}),
|
||||
},
|
||||
{
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
instanceInfo: nilPrivateIP,
|
||||
},
|
||||
{
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
instanceInfo: nilMachine,
|
||||
},
|
||||
{
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
instanceInfo: nilMachineState,
|
||||
},
|
||||
{
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
instanceInfo: nilMachineStateName,
|
||||
},
|
||||
{
|
||||
expected: false,
|
||||
exposedByDefault: true,
|
||||
instanceInfo: invalidMachineState,
|
||||
},
|
||||
}
|
||||
|
||||
for i, c := range cases {
|
||||
provider := &ECS{
|
||||
ExposedByDefault: c.exposedByDefault,
|
||||
}
|
||||
value := provider.filterInstance(c.instanceInfo)
|
||||
if value != c.expected {
|
||||
t.Fatalf("Should have been %v, got %v (case %d)", c.expected, value, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskChunking(t *testing.T) {
|
||||
provider := &ECS{}
|
||||
|
||||
testval := "a"
|
||||
cases := []struct {
|
||||
count int
|
||||
expectedLengths []int
|
||||
}{
|
||||
{0, []int(nil)},
|
||||
{1, []int{1}},
|
||||
{99, []int{99}},
|
||||
{100, []int{100}},
|
||||
{101, []int{100, 1}},
|
||||
{199, []int{100, 99}},
|
||||
{200, []int{100, 100}},
|
||||
{201, []int{100, 100, 1}},
|
||||
{555, []int{100, 100, 100, 100, 100, 55}},
|
||||
{1001, []int{100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 1}},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
var tasks []*string
|
||||
for v := 0; v < c.count; v++ {
|
||||
tasks = append(tasks, &testval)
|
||||
}
|
||||
|
||||
out := provider.chunkedTaskArns(tasks)
|
||||
var outCount []int
|
||||
|
||||
for _, el := range out {
|
||||
outCount = append(outCount, len(el))
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(outCount, c.expectedLengths) {
|
||||
t.Errorf("Chunking %d elements, expected %#v, got %#v", c.count, c.expectedLengths, outCount)
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@ package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
|
144
provider/eureka.go
Normal file
144
provider/eureka.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/ArthurHlt/go-eureka-client/eureka"
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/cenk/backoff"
|
||||
"github.com/containous/traefik/job"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
)
|
||||
|
||||
// Eureka holds configuration of the Eureka provider.
|
||||
type Eureka struct {
|
||||
BaseProvider `mapstructure:",squash"`
|
||||
Endpoint string
|
||||
Delay string
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Eureka) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, _ types.Constraints) error {
|
||||
|
||||
operation := func() error {
|
||||
configuration, err := provider.buildConfiguration()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to build configuration for Eureka, error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "eureka",
|
||||
Configuration: configuration,
|
||||
}
|
||||
|
||||
var delay time.Duration
|
||||
if len(provider.Delay) > 0 {
|
||||
var err error
|
||||
delay, err = time.ParseDuration(provider.Delay)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to parse delay for Eureka, error: %s", err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
delay = time.Second * 30
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(delay)
|
||||
go func() {
|
||||
for t := range ticker.C {
|
||||
|
||||
log.Debug("Refreshing Eureka " + t.String())
|
||||
|
||||
configuration, err := provider.buildConfiguration()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to refresh Eureka configuration, error: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "eureka",
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Eureka connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot connect to Eureka server %+v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build the configuration from Eureka server
|
||||
func (provider *Eureka) buildConfiguration() (*types.Configuration, error) {
|
||||
var EurekaFuncMap = template.FuncMap{
|
||||
"getPort": provider.getPort,
|
||||
"getProtocol": provider.getProtocol,
|
||||
"getWeight": provider.getWeight,
|
||||
"getInstanceID": provider.getInstanceID,
|
||||
}
|
||||
|
||||
eureka.GetLogger().SetOutput(ioutil.Discard)
|
||||
|
||||
client := eureka.NewClient([]string{
|
||||
provider.Endpoint,
|
||||
})
|
||||
|
||||
applications, err := client.GetApplications()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
templateObjects := struct {
|
||||
Applications []eureka.Application
|
||||
}{
|
||||
applications.Applications,
|
||||
}
|
||||
|
||||
configuration, err := provider.getConfiguration("templates/eureka.tmpl", EurekaFuncMap, templateObjects)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
return configuration, nil
|
||||
}
|
||||
|
||||
func (provider *Eureka) getPort(instance eureka.InstanceInfo) string {
|
||||
if instance.SecurePort.Enabled {
|
||||
return strconv.Itoa(instance.SecurePort.Port)
|
||||
}
|
||||
return strconv.Itoa(instance.Port.Port)
|
||||
}
|
||||
|
||||
func (provider *Eureka) getProtocol(instance eureka.InstanceInfo) string {
|
||||
if instance.SecurePort.Enabled {
|
||||
return "https"
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (provider *Eureka) getWeight(instance eureka.InstanceInfo) string {
|
||||
if val, ok := instance.Metadata.Map["traefik.weight"]; ok {
|
||||
return val
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func (provider *Eureka) getInstanceID(instance eureka.InstanceInfo) string {
|
||||
if val, ok := instance.Metadata.Map["traefik.backend.id"]; ok {
|
||||
return val
|
||||
}
|
||||
return strings.Replace(instance.IpAddr, ".", "-", -1) + "-" + provider.getPort(instance)
|
||||
}
|
171
provider/eureka_test.go
Normal file
171
provider/eureka_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/ArthurHlt/go-eureka-client/eureka"
|
||||
)
|
||||
|
||||
func TestEurekaGetPort(t *testing.T) {
|
||||
cases := []struct {
|
||||
expectedPort string
|
||||
instanceInfo eureka.InstanceInfo
|
||||
}{
|
||||
{
|
||||
expectedPort: "80",
|
||||
instanceInfo: eureka.InstanceInfo{
|
||||
SecurePort: &eureka.Port{
|
||||
Port: 443, Enabled: false,
|
||||
},
|
||||
Port: &eureka.Port{
|
||||
Port: 80, Enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expectedPort: "443",
|
||||
instanceInfo: eureka.InstanceInfo{
|
||||
SecurePort: &eureka.Port{
|
||||
Port: 443, Enabled: true,
|
||||
},
|
||||
Port: &eureka.Port{
|
||||
Port: 80, Enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
eurekaProvider := &Eureka{}
|
||||
for _, c := range cases {
|
||||
port := eurekaProvider.getPort(c.instanceInfo)
|
||||
if port != c.expectedPort {
|
||||
t.Fatalf("Should have been %s, got %s", c.expectedPort, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEurekaGetProtocol(t *testing.T) {
|
||||
cases := []struct {
|
||||
expectedProtocol string
|
||||
instanceInfo eureka.InstanceInfo
|
||||
}{
|
||||
{
|
||||
expectedProtocol: "http",
|
||||
instanceInfo: eureka.InstanceInfo{
|
||||
SecurePort: &eureka.Port{
|
||||
Port: 443, Enabled: false,
|
||||
},
|
||||
Port: &eureka.Port{
|
||||
Port: 80, Enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expectedProtocol: "https",
|
||||
instanceInfo: eureka.InstanceInfo{
|
||||
SecurePort: &eureka.Port{
|
||||
Port: 443, Enabled: true,
|
||||
},
|
||||
Port: &eureka.Port{
|
||||
Port: 80, Enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
eurekaProvider := &Eureka{}
|
||||
for _, c := range cases {
|
||||
protocol := eurekaProvider.getProtocol(c.instanceInfo)
|
||||
if protocol != c.expectedProtocol {
|
||||
t.Fatalf("Should have been %s, got %s", c.expectedProtocol, protocol)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEurekaGetWeight(t *testing.T) {
|
||||
cases := []struct {
|
||||
expectedWeight string
|
||||
instanceInfo eureka.InstanceInfo
|
||||
}{
|
||||
{
|
||||
expectedWeight: "0",
|
||||
instanceInfo: eureka.InstanceInfo{
|
||||
Port: &eureka.Port{
|
||||
Port: 80, Enabled: true,
|
||||
},
|
||||
Metadata: &eureka.MetaData{
|
||||
Map: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expectedWeight: "10",
|
||||
instanceInfo: eureka.InstanceInfo{
|
||||
Port: &eureka.Port{
|
||||
Port: 80, Enabled: true,
|
||||
},
|
||||
Metadata: &eureka.MetaData{
|
||||
Map: map[string]string{
|
||||
"traefik.weight": "10",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
eurekaProvider := &Eureka{}
|
||||
for _, c := range cases {
|
||||
weight := eurekaProvider.getWeight(c.instanceInfo)
|
||||
if weight != c.expectedWeight {
|
||||
t.Fatalf("Should have been %s, got %s", c.expectedWeight, weight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEurekaGetInstanceId(t *testing.T) {
|
||||
cases := []struct {
|
||||
expectedID string
|
||||
instanceInfo eureka.InstanceInfo
|
||||
}{
|
||||
{
|
||||
expectedID: "MyInstanceId",
|
||||
instanceInfo: eureka.InstanceInfo{
|
||||
IpAddr: "10.11.12.13",
|
||||
SecurePort: &eureka.Port{
|
||||
Port: 443, Enabled: false,
|
||||
},
|
||||
Port: &eureka.Port{
|
||||
Port: 80, Enabled: true,
|
||||
},
|
||||
Metadata: &eureka.MetaData{
|
||||
Map: map[string]string{
|
||||
"traefik.backend.id": "MyInstanceId",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
expectedID: "10-11-12-13-80",
|
||||
instanceInfo: eureka.InstanceInfo{
|
||||
IpAddr: "10.11.12.13",
|
||||
SecurePort: &eureka.Port{
|
||||
Port: 443, Enabled: false,
|
||||
},
|
||||
Port: &eureka.Port{
|
||||
Port: 80, Enabled: true,
|
||||
},
|
||||
Metadata: &eureka.MetaData{
|
||||
Map: map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
eurekaProvider := &Eureka{}
|
||||
for _, c := range cases {
|
||||
id := eurekaProvider.getInstanceID(c.instanceInfo)
|
||||
if id != c.expectedID {
|
||||
t.Fatalf("Should have been %s, got %s", c.expectedID, id)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,300 +1,249 @@
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/parnurzeal/gorequest"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"k8s.io/client-go/1.5/kubernetes"
|
||||
"k8s.io/client-go/1.5/pkg/api"
|
||||
"k8s.io/client-go/1.5/pkg/api/v1"
|
||||
"k8s.io/client-go/1.5/pkg/apis/extensions/v1beta1"
|
||||
"k8s.io/client-go/1.5/pkg/fields"
|
||||
"k8s.io/client-go/1.5/pkg/labels"
|
||||
"k8s.io/client-go/1.5/pkg/runtime"
|
||||
"k8s.io/client-go/1.5/pkg/watch"
|
||||
"k8s.io/client-go/1.5/rest"
|
||||
"k8s.io/client-go/1.5/tools/cache"
|
||||
)
|
||||
|
||||
const (
|
||||
// APIEndpoint defines the base path for kubernetes API resources.
|
||||
APIEndpoint = "/api/v1"
|
||||
extentionsEndpoint = "/apis/extensions/v1beta1"
|
||||
defaultIngress = "/ingresses"
|
||||
namespaces = "/namespaces/"
|
||||
)
|
||||
const resyncPeriod = time.Minute * 5
|
||||
|
||||
// Client is a client for the Kubernetes master.
|
||||
// WatchAll starts the watch of the Kubernetes ressources and updates the stores.
|
||||
// The stores can then be accessed via the Get* functions.
|
||||
type Client interface {
|
||||
GetIngresses(labelSelector string, predicate func(Ingress) bool) ([]Ingress, error)
|
||||
GetService(name, namespace string) (Service, error)
|
||||
GetEndpoints(name, namespace string) (Endpoints, error)
|
||||
WatchAll(labelSelector string, stopCh <-chan bool) (chan interface{}, chan error, error)
|
||||
GetIngresses(namespaces Namespaces) []*v1beta1.Ingress
|
||||
GetService(namespace, name string) (*v1.Service, bool, error)
|
||||
GetEndpoints(namespace, name string) (*v1.Endpoints, bool, error)
|
||||
WatchAll(labelSelector string, stopCh <-chan struct{}) (<-chan interface{}, error)
|
||||
}
|
||||
|
||||
type clientImpl struct {
|
||||
endpointURL string
|
||||
tls *tls.Config
|
||||
token string
|
||||
caCert []byte
|
||||
ingController *cache.Controller
|
||||
svcController *cache.Controller
|
||||
epController *cache.Controller
|
||||
|
||||
ingStore cache.Store
|
||||
svcStore cache.Store
|
||||
epStore cache.Store
|
||||
|
||||
clientset *kubernetes.Clientset
|
||||
}
|
||||
|
||||
// NewClient returns a new Kubernetes client.
|
||||
// The provided host is an url (scheme://hostname[:port]) of a
|
||||
// Kubernetes master without any path.
|
||||
// The provided client is an authorized http.Client used to perform requests to the Kubernetes API master.
|
||||
func NewClient(baseURL string, caCert []byte, token string) (Client, error) {
|
||||
validURL, err := url.Parse(baseURL)
|
||||
// NewClient returns a new Kubernetes client
|
||||
func NewClient(endpoint string) (Client, error) {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse URL %q: %v", baseURL, err)
|
||||
log.Warnf("Kubernetes in cluster config error, trying from out of cluster: %s", err)
|
||||
config = &rest.Config{}
|
||||
}
|
||||
|
||||
if len(endpoint) > 0 {
|
||||
config.Host = endpoint
|
||||
}
|
||||
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &clientImpl{
|
||||
endpointURL: strings.TrimSuffix(validURL.String(), "/"),
|
||||
token: token,
|
||||
caCert: caCert,
|
||||
clientset: clientset,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func makeQueryString(baseParams map[string]string, labelSelector string) (string, error) {
|
||||
if labelSelector != "" {
|
||||
baseParams["labelSelector"] = labelSelector
|
||||
}
|
||||
queryData, err := json.Marshal(baseParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(queryData), nil
|
||||
}
|
||||
|
||||
// GetIngresses returns all ingresses in the cluster
|
||||
func (c *clientImpl) GetIngresses(labelSelector string, predicate func(Ingress) bool) ([]Ingress, error) {
|
||||
getURL := c.endpointURL + extentionsEndpoint + defaultIngress
|
||||
queryParams := map[string]string{}
|
||||
queryData, err := makeQueryString(queryParams, labelSelector)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Had problems constructing query string %s : %v", queryParams, err)
|
||||
}
|
||||
body, err := c.do(c.request(getURL, queryData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create ingresses request: GET %q : %v", getURL, err)
|
||||
}
|
||||
func (c *clientImpl) GetIngresses(namespaces Namespaces) []*v1beta1.Ingress {
|
||||
ingList := c.ingStore.List()
|
||||
result := make([]*v1beta1.Ingress, 0, len(ingList))
|
||||
|
||||
var ingressList IngressList
|
||||
if err := json.Unmarshal(body, &ingressList); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode list of ingress resources: %v", err)
|
||||
}
|
||||
ingresses := ingressList.Items[:0]
|
||||
for _, ingress := range ingressList.Items {
|
||||
if predicate(ingress) {
|
||||
ingresses = append(ingresses, ingress)
|
||||
for _, obj := range ingList {
|
||||
ingress := obj.(*v1beta1.Ingress)
|
||||
if HasNamespace(ingress, namespaces) {
|
||||
result = append(result, ingress)
|
||||
}
|
||||
}
|
||||
return ingresses, nil
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// WatchIngresses returns all ingresses in the cluster
|
||||
func (c *clientImpl) WatchIngresses(labelSelector string, stopCh <-chan bool) (chan interface{}, chan error, error) {
|
||||
getURL := c.endpointURL + extentionsEndpoint + defaultIngress
|
||||
return c.watch(getURL, labelSelector, stopCh)
|
||||
// WatchIngresses starts the watch of Kubernetes Ingresses resources and updates the corresponding store
|
||||
func (c *clientImpl) WatchIngresses(labelSelector labels.Selector, watchCh chan<- interface{}, stopCh <-chan struct{}) {
|
||||
source := NewListWatchFromClient(
|
||||
c.clientset.ExtensionsClient,
|
||||
"ingresses",
|
||||
api.NamespaceAll,
|
||||
fields.Everything(),
|
||||
labelSelector)
|
||||
|
||||
c.ingStore, c.ingController = cache.NewInformer(
|
||||
source,
|
||||
&v1beta1.Ingress{},
|
||||
resyncPeriod,
|
||||
newResourceEventHandlerFuncs(watchCh))
|
||||
go c.ingController.Run(stopCh)
|
||||
}
|
||||
|
||||
// eventHandlerFunc will pass the obj on to the events channel or drop it
|
||||
// This is so passing the events along won't block in the case of high volume
|
||||
// The events are only used for signalling anyway so dropping a few is ok
|
||||
func eventHandlerFunc(events chan<- interface{}, obj interface{}) {
|
||||
select {
|
||||
case events <- obj:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func newResourceEventHandlerFuncs(events chan<- interface{}) cache.ResourceEventHandlerFuncs {
|
||||
return cache.ResourceEventHandlerFuncs{
|
||||
AddFunc: func(obj interface{}) { eventHandlerFunc(events, obj) },
|
||||
UpdateFunc: func(old, new interface{}) { eventHandlerFunc(events, new) },
|
||||
DeleteFunc: func(obj interface{}) { eventHandlerFunc(events, obj) },
|
||||
}
|
||||
}
|
||||
|
||||
// GetService returns the named service from the named namespace
|
||||
func (c *clientImpl) GetService(name, namespace string) (Service, error) {
|
||||
getURL := c.endpointURL + APIEndpoint + namespaces + namespace + "/services/" + name
|
||||
|
||||
body, err := c.do(c.request(getURL, ""))
|
||||
if err != nil {
|
||||
return Service{}, fmt.Errorf("failed to create services request: GET %q : %v", getURL, err)
|
||||
func (c *clientImpl) GetService(namespace, name string) (*v1.Service, bool, error) {
|
||||
var service *v1.Service
|
||||
item, exists, err := c.svcStore.GetByKey(namespace + "/" + name)
|
||||
if item != nil {
|
||||
service = item.(*v1.Service)
|
||||
}
|
||||
|
||||
var service Service
|
||||
if err := json.Unmarshal(body, &service); err != nil {
|
||||
return Service{}, fmt.Errorf("failed to decode service resource: %v", err)
|
||||
}
|
||||
return service, nil
|
||||
return service, exists, err
|
||||
}
|
||||
|
||||
// WatchServices returns all services in the cluster
|
||||
func (c *clientImpl) WatchServices(stopCh <-chan bool) (chan interface{}, chan error, error) {
|
||||
getURL := c.endpointURL + APIEndpoint + "/services"
|
||||
return c.watch(getURL, "", stopCh)
|
||||
// WatchServices starts the watch of Kubernetes Service resources and updates the corresponding store
|
||||
func (c *clientImpl) WatchServices(watchCh chan<- interface{}, stopCh <-chan struct{}) {
|
||||
source := cache.NewListWatchFromClient(
|
||||
c.clientset.CoreClient,
|
||||
"services",
|
||||
api.NamespaceAll,
|
||||
fields.Everything())
|
||||
|
||||
c.svcStore, c.svcController = cache.NewInformer(
|
||||
source,
|
||||
&v1.Service{},
|
||||
resyncPeriod,
|
||||
newResourceEventHandlerFuncs(watchCh))
|
||||
go c.svcController.Run(stopCh)
|
||||
}
|
||||
|
||||
// GetEndpoints returns the named Endpoints
|
||||
// Endpoints have the same name as the coresponding service
|
||||
func (c *clientImpl) GetEndpoints(name, namespace string) (Endpoints, error) {
|
||||
getURL := c.endpointURL + APIEndpoint + namespaces + namespace + "/endpoints/" + name
|
||||
func (c *clientImpl) GetEndpoints(namespace, name string) (*v1.Endpoints, bool, error) {
|
||||
var endpoint *v1.Endpoints
|
||||
item, exists, err := c.epStore.GetByKey(namespace + "/" + name)
|
||||
|
||||
body, err := c.do(c.request(getURL, ""))
|
||||
if err != nil {
|
||||
return Endpoints{}, fmt.Errorf("failed to create endpoints request: GET %q : %v", getURL, err)
|
||||
if item != nil {
|
||||
endpoint = item.(*v1.Endpoints)
|
||||
}
|
||||
|
||||
var endpoints Endpoints
|
||||
if err := json.Unmarshal(body, &endpoints); err != nil {
|
||||
return Endpoints{}, fmt.Errorf("failed to decode endpoints resources: %v", err)
|
||||
}
|
||||
return endpoints, nil
|
||||
return endpoint, exists, err
|
||||
}
|
||||
|
||||
// WatchEndpoints returns endpoints in the cluster
|
||||
func (c *clientImpl) WatchEndpoints(stopCh <-chan bool) (chan interface{}, chan error, error) {
|
||||
getURL := c.endpointURL + APIEndpoint + "/endpoints"
|
||||
return c.watch(getURL, "", stopCh)
|
||||
// WatchEndpoints starts the watch of Kubernetes Endpoints resources and updates the corresponding store
|
||||
func (c *clientImpl) WatchEndpoints(watchCh chan<- interface{}, stopCh <-chan struct{}) {
|
||||
source := cache.NewListWatchFromClient(
|
||||
c.clientset.CoreClient,
|
||||
"endpoints",
|
||||
api.NamespaceAll,
|
||||
fields.Everything())
|
||||
|
||||
c.epStore, c.epController = cache.NewInformer(
|
||||
source,
|
||||
&v1.Endpoints{},
|
||||
resyncPeriod,
|
||||
newResourceEventHandlerFuncs(watchCh))
|
||||
go c.epController.Run(stopCh)
|
||||
}
|
||||
|
||||
// WatchAll returns events in the cluster
|
||||
func (c *clientImpl) WatchAll(labelSelector string, stopCh <-chan bool) (chan interface{}, chan error, error) {
|
||||
watchCh := make(chan interface{}, 100)
|
||||
errCh := make(chan error, 100)
|
||||
// WatchAll returns events in the cluster and updates the stores via informer
|
||||
// Filters ingresses by labelSelector
|
||||
func (c *clientImpl) WatchAll(labelSelector string, stopCh <-chan struct{}) (<-chan interface{}, error) {
|
||||
watchCh := make(chan interface{}, 1)
|
||||
eventCh := make(chan interface{}, 1)
|
||||
|
||||
stopIngresses := make(chan bool, 10)
|
||||
chanIngresses, chanIngressesErr, err := c.WatchIngresses(labelSelector, stopIngresses)
|
||||
kubeLabelSelector, err := labels.Parse(labelSelector)
|
||||
if err != nil {
|
||||
return watchCh, errCh, fmt.Errorf("failed to create watch: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
stopServices := make(chan bool, 10)
|
||||
chanServices, chanServicesErr, err := c.WatchServices(stopServices)
|
||||
if err != nil {
|
||||
return watchCh, errCh, fmt.Errorf("failed to create watch: %v", err)
|
||||
}
|
||||
stopEndpoints := make(chan bool, 10)
|
||||
chanEndpoints, chanEndpointsErr, err := c.WatchEndpoints(stopEndpoints)
|
||||
if err != nil {
|
||||
return watchCh, errCh, fmt.Errorf("failed to create watch: %v", err)
|
||||
}
|
||||
safe.Go(func() {
|
||||
|
||||
c.WatchIngresses(kubeLabelSelector, eventCh, stopCh)
|
||||
c.WatchServices(eventCh, stopCh)
|
||||
c.WatchEndpoints(eventCh, stopCh)
|
||||
|
||||
go func() {
|
||||
defer close(watchCh)
|
||||
defer close(errCh)
|
||||
defer close(stopIngresses)
|
||||
defer close(stopServices)
|
||||
defer close(stopEndpoints)
|
||||
defer close(eventCh)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopCh:
|
||||
stopIngresses <- true
|
||||
stopServices <- true
|
||||
stopEndpoints <- true
|
||||
return
|
||||
case err := <-chanIngressesErr:
|
||||
errCh <- err
|
||||
case err := <-chanServicesErr:
|
||||
errCh <- err
|
||||
case err := <-chanEndpointsErr:
|
||||
errCh <- err
|
||||
case event := <-chanIngresses:
|
||||
watchCh <- event
|
||||
case event := <-chanServices:
|
||||
watchCh <- event
|
||||
case event := <-chanEndpoints:
|
||||
watchCh <- event
|
||||
case event := <-eventCh:
|
||||
c.fireEvent(event, watchCh)
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
return watchCh, errCh, nil
|
||||
return watchCh, nil
|
||||
}
|
||||
|
||||
func (c *clientImpl) do(request *gorequest.SuperAgent) ([]byte, error) {
|
||||
res, body, errs := request.EndBytes()
|
||||
if errs != nil {
|
||||
return nil, fmt.Errorf("failed to create request: GET %q : %v", request.Url, errs)
|
||||
// fireEvent checks if all controllers have synced before firing
|
||||
// Used after startup or a reconnect
|
||||
func (c *clientImpl) fireEvent(event interface{}, eventCh chan interface{}) {
|
||||
if !c.ingController.HasSynced() || !c.svcController.HasSynced() || !c.epController.HasSynced() {
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("http error %d GET %q: %q", res.StatusCode, request.Url, string(body))
|
||||
}
|
||||
return body, nil
|
||||
eventHandlerFunc(eventCh, event)
|
||||
}
|
||||
|
||||
func (c *clientImpl) request(reqURL string, queryContent interface{}) *gorequest.SuperAgent {
|
||||
// Make request to Kubernetes API
|
||||
parsedURL, parseErr := url.Parse(reqURL)
|
||||
if parseErr != nil {
|
||||
log.Errorf("Had issues parsing url %s. Trying anyway.", reqURL)
|
||||
// HasNamespace checks if the ingress is in one of the namespaces
|
||||
func HasNamespace(ingress *v1beta1.Ingress, namespaces Namespaces) bool {
|
||||
if len(namespaces) == 0 {
|
||||
return true
|
||||
}
|
||||
request := gorequest.New().Get(reqURL)
|
||||
request.Transport.DisableKeepAlives = true
|
||||
|
||||
if parsedURL.Scheme == "https" {
|
||||
pool := x509.NewCertPool()
|
||||
pool.AppendCertsFromPEM(c.caCert)
|
||||
c.tls = &tls.Config{RootCAs: pool}
|
||||
request.TLSClientConfig(c.tls)
|
||||
for _, n := range namespaces {
|
||||
if ingress.ObjectMeta.Namespace == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if len(c.token) > 0 {
|
||||
request.Header["Authorization"] = "Bearer " + c.token
|
||||
}
|
||||
request.Query(queryContent)
|
||||
return request
|
||||
return false
|
||||
}
|
||||
|
||||
// GenericObject generic object
|
||||
type GenericObject struct {
|
||||
TypeMeta `json:",inline"`
|
||||
ListMeta `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
func (c *clientImpl) watch(url string, labelSelector string, stopCh <-chan bool) (chan interface{}, chan error, error) {
|
||||
watchCh := make(chan interface{}, 10)
|
||||
errCh := make(chan error, 10)
|
||||
|
||||
// get version
|
||||
body, err := c.do(c.request(url, ""))
|
||||
if err != nil {
|
||||
return watchCh, errCh, fmt.Errorf("failed to do version request: GET %q : %v", url, err)
|
||||
}
|
||||
|
||||
var generic GenericObject
|
||||
if err := json.Unmarshal(body, &generic); err != nil {
|
||||
return watchCh, errCh, fmt.Errorf("failed to decode version %v", err)
|
||||
}
|
||||
resourceVersion := generic.ResourceVersion
|
||||
queryParams := map[string]string{"watch": "true", "resourceVersion": resourceVersion}
|
||||
queryData, err := makeQueryString(queryParams, labelSelector)
|
||||
if err != nil {
|
||||
return watchCh, errCh, fmt.Errorf("Unable to construct query args")
|
||||
}
|
||||
request := c.request(url, queryData)
|
||||
req, err := request.MakeRequest()
|
||||
if err != nil {
|
||||
return watchCh, errCh, fmt.Errorf("failed to make watch request: GET %q : %v", url, err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
req = req.WithContext(ctx)
|
||||
request.Client.Transport = request.Transport
|
||||
|
||||
res, err := request.Client.Do(req)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return watchCh, errCh, fmt.Errorf("failed to do watch request: GET %q: %v", url, err)
|
||||
}
|
||||
|
||||
safe.Go(func() {
|
||||
EndCh := make(chan bool, 1)
|
||||
defer close(watchCh)
|
||||
defer close(errCh)
|
||||
defer close(EndCh)
|
||||
safe.Go(func() {
|
||||
defer res.Body.Close()
|
||||
defer func() {
|
||||
EndCh <- true
|
||||
}()
|
||||
for {
|
||||
var eventList interface{}
|
||||
if err := json.NewDecoder(res.Body).Decode(&eventList); err != nil {
|
||||
if !strings.Contains(err.Error(), "net/http: request canceled") {
|
||||
errCh <- fmt.Errorf("failed to decode watch event: GET %q : %v", url, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
watchCh <- eventList
|
||||
}
|
||||
})
|
||||
<-stopCh
|
||||
safe.Go(func() {
|
||||
cancel() // cancel watch request
|
||||
})
|
||||
<-EndCh
|
||||
})
|
||||
return watchCh, errCh, nil
|
||||
// NewListWatchFromClient creates a new ListWatch from the specified client, resource, namespace, field selector and label selector.
|
||||
// Extends cache.NewListWatchFromClient to support labelSelector
|
||||
func NewListWatchFromClient(c cache.Getter, resource string, namespace string, fieldSelector fields.Selector, labelSelector labels.Selector) *cache.ListWatch {
|
||||
listFunc := func(options api.ListOptions) (runtime.Object, error) {
|
||||
return c.Get().
|
||||
Namespace(namespace).
|
||||
Resource(resource).
|
||||
VersionedParams(&options, api.ParameterCodec).
|
||||
FieldsSelectorParam(fieldSelector).
|
||||
LabelsSelectorParam(labelSelector).
|
||||
Do().
|
||||
Get()
|
||||
}
|
||||
watchFunc := func(options api.ListOptions) (watch.Interface, error) {
|
||||
return c.Get().
|
||||
Prefix("watch").
|
||||
Namespace(namespace).
|
||||
Resource(resource).
|
||||
VersionedParams(&options, api.ParameterCodec).
|
||||
FieldsSelectorParam(fieldSelector).
|
||||
LabelsSelectorParam(labelSelector).
|
||||
Watch()
|
||||
}
|
||||
return &cache.ListWatch{ListFunc: listFunc, WatchFunc: watchFunc}
|
||||
}
|
||||
|
@@ -1,84 +0,0 @@
|
||||
package k8s
|
||||
|
||||
// Endpoints is a collection of endpoints that implement the actual service. Example:
|
||||
// Name: "mysvc",
|
||||
// Subsets: [
|
||||
// {
|
||||
// Addresses: [{"ip": "10.10.1.1"}, {"ip": "10.10.2.2"}],
|
||||
// Ports: [{"name": "a", "port": 8675}, {"name": "b", "port": 309}]
|
||||
// },
|
||||
// {
|
||||
// Addresses: [{"ip": "10.10.3.3"}],
|
||||
// Ports: [{"name": "a", "port": 93}, {"name": "b", "port": 76}]
|
||||
// },
|
||||
// ]
|
||||
type Endpoints struct {
|
||||
TypeMeta `json:",inline"`
|
||||
ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// The set of all endpoints is the union of all subsets.
|
||||
Subsets []EndpointSubset
|
||||
}
|
||||
|
||||
// EndpointSubset is a group of addresses with a common set of ports. The
|
||||
// expanded set of endpoints is the Cartesian product of Addresses x Ports.
|
||||
// For example, given:
|
||||
// {
|
||||
// Addresses: [{"ip": "10.10.1.1"}, {"ip": "10.10.2.2"}],
|
||||
// Ports: [{"name": "a", "port": 8675}, {"name": "b", "port": 309}]
|
||||
// }
|
||||
// The resulting set of endpoints can be viewed as:
|
||||
// a: [ 10.10.1.1:8675, 10.10.2.2:8675 ],
|
||||
// b: [ 10.10.1.1:309, 10.10.2.2:309 ]
|
||||
type EndpointSubset struct {
|
||||
Addresses []EndpointAddress
|
||||
NotReadyAddresses []EndpointAddress
|
||||
Ports []EndpointPort
|
||||
}
|
||||
|
||||
// EndpointAddress is a tuple that describes single IP address.
|
||||
type EndpointAddress struct {
|
||||
// The IP of this endpoint.
|
||||
// IPv6 is also accepted but not fully supported on all platforms. Also, certain
|
||||
// kubernetes components, like kube-proxy, are not IPv6 ready.
|
||||
// TODO: This should allow hostname or IP, see #4447.
|
||||
IP string
|
||||
// Optional: Hostname of this endpoint
|
||||
// Meant to be used by DNS servers etc.
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
// Optional: The kubernetes object related to the entry point.
|
||||
TargetRef *ObjectReference
|
||||
}
|
||||
|
||||
// EndpointPort is a tuple that describes a single port.
|
||||
type EndpointPort struct {
|
||||
// The name of this port (corresponds to ServicePort.Name). Optional
|
||||
// if only one port is defined. Must be a DNS_LABEL.
|
||||
Name string
|
||||
|
||||
// The port number.
|
||||
Port int32
|
||||
|
||||
// The IP protocol for this port.
|
||||
Protocol Protocol
|
||||
}
|
||||
|
||||
// ObjectReference contains enough information to let you inspect or modify the referred object.
|
||||
type ObjectReference struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
UID UID `json:"uid,omitempty"`
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
ResourceVersion string `json:"resourceVersion,omitempty"`
|
||||
|
||||
// Optional. If referring to a piece of an object instead of an entire object, this string
|
||||
// should contain information to identify the sub-object. For example, if the object
|
||||
// reference is to a container within a pod, this would take on a value like:
|
||||
// "spec.containers{name}" (where "name" refers to the name of the container that triggered
|
||||
// the event) or if no container name is specified "spec.containers[2]" (container with
|
||||
// index 2 in this pod). This syntax is chosen only to have some well-defined way of
|
||||
// referencing a part of an object.
|
||||
// TODO: this design is not final and this field is subject to change in the future.
|
||||
FieldPath string `json:"fieldPath,omitempty"`
|
||||
}
|
@@ -1,151 +0,0 @@
|
||||
package k8s
|
||||
|
||||
// Ingress is a collection of rules that allow inbound connections to reach the
|
||||
// endpoints defined by a backend. An Ingress can be configured to give services
|
||||
// externally-reachable urls, load balance traffic, terminate SSL, offer name
|
||||
// based virtual hosting etc.
|
||||
type Ingress struct {
|
||||
TypeMeta `json:",inline"`
|
||||
// Standard object's metadata.
|
||||
// More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata
|
||||
ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec is the desired state of the Ingress.
|
||||
// More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status
|
||||
Spec IngressSpec `json:"spec,omitempty"`
|
||||
|
||||
// Status is the current state of the Ingress.
|
||||
// More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status
|
||||
Status IngressStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// IngressList is a collection of Ingress.
|
||||
type IngressList struct {
|
||||
TypeMeta `json:",inline"`
|
||||
// Standard object's metadata.
|
||||
// More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata
|
||||
ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Items is the list of Ingress.
|
||||
Items []Ingress `json:"items"`
|
||||
}
|
||||
|
||||
// IngressSpec describes the Ingress the user wishes to exist.
|
||||
type IngressSpec struct {
|
||||
// A default backend capable of servicing requests that don't match any
|
||||
// rule. At least one of 'backend' or 'rules' must be specified. This field
|
||||
// is optional to allow the loadbalancer controller or defaulting logic to
|
||||
// specify a global default.
|
||||
Backend *IngressBackend `json:"backend,omitempty"`
|
||||
|
||||
// TLS configuration. Currently the Ingress only supports a single TLS
|
||||
// port, 443. If multiple members of this list specify different hosts, they
|
||||
// will be multiplexed on the same port according to the hostname specified
|
||||
// through the SNI TLS extension, if the ingress controller fulfilling the
|
||||
// ingress supports SNI.
|
||||
TLS []IngressTLS `json:"tls,omitempty"`
|
||||
|
||||
// A list of host rules used to configure the Ingress. If unspecified, or
|
||||
// no rule matches, all traffic is sent to the default backend.
|
||||
Rules []IngressRule `json:"rules,omitempty"`
|
||||
// TODO: Add the ability to specify load-balancer IP through claims
|
||||
}
|
||||
|
||||
// IngressTLS describes the transport layer security associated with an Ingress.
|
||||
type IngressTLS struct {
|
||||
// Hosts are a list of hosts included in the TLS certificate. The values in
|
||||
// this list must match the name/s used in the tlsSecret. Defaults to the
|
||||
// wildcard host setting for the loadbalancer controller fulfilling this
|
||||
// Ingress, if left unspecified.
|
||||
Hosts []string `json:"hosts,omitempty"`
|
||||
// SecretName is the name of the secret used to terminate SSL traffic on 443.
|
||||
// Field is left optional to allow SSL routing based on SNI hostname alone.
|
||||
// If the SNI host in a listener conflicts with the "Host" header field used
|
||||
// by an IngressRule, the SNI host is used for termination and value of the
|
||||
// Host header is used for routing.
|
||||
SecretName string `json:"secretName,omitempty"`
|
||||
// TODO: Consider specifying different modes of termination, protocols etc.
|
||||
}
|
||||
|
||||
// IngressStatus describe the current state of the Ingress.
|
||||
type IngressStatus struct {
|
||||
// LoadBalancer contains the current status of the load-balancer.
|
||||
LoadBalancer LoadBalancerStatus `json:"loadBalancer,omitempty"`
|
||||
}
|
||||
|
||||
// IngressRule represents the rules mapping the paths under a specified host to
|
||||
// the related backend services. Incoming requests are first evaluated for a host
|
||||
// match, then routed to the backend associated with the matching IngressRuleValue.
|
||||
type IngressRule struct {
|
||||
// Host is the fully qualified domain name of a network host, as defined
|
||||
// by RFC 3986. Note the following deviations from the "host" part of the
|
||||
// URI as defined in the RFC:
|
||||
// 1. IPs are not allowed. Currently an IngressRuleValue can only apply to the
|
||||
// IP in the Spec of the parent Ingress.
|
||||
// 2. The `:` delimiter is not respected because ports are not allowed.
|
||||
// Currently the port of an Ingress is implicitly :80 for http and
|
||||
// :443 for https.
|
||||
// Both these may change in the future.
|
||||
// Incoming requests are matched against the host before the IngressRuleValue.
|
||||
// If the host is unspecified, the Ingress routes all traffic based on the
|
||||
// specified IngressRuleValue.
|
||||
Host string `json:"host,omitempty"`
|
||||
// IngressRuleValue represents a rule to route requests for this IngressRule.
|
||||
// If unspecified, the rule defaults to a http catch-all. Whether that sends
|
||||
// just traffic matching the host to the default backend or all traffic to the
|
||||
// default backend, is left to the controller fulfilling the Ingress. Http is
|
||||
// currently the only supported IngressRuleValue.
|
||||
IngressRuleValue `json:",inline,omitempty"`
|
||||
}
|
||||
|
||||
// IngressRuleValue represents a rule to apply against incoming requests. If the
|
||||
// rule is satisfied, the request is routed to the specified backend. Currently
|
||||
// mixing different types of rules in a single Ingress is disallowed, so exactly
|
||||
// one of the following must be set.
|
||||
type IngressRuleValue struct {
|
||||
//TODO:
|
||||
// 1. Consider renaming this resource and the associated rules so they
|
||||
// aren't tied to Ingress. They can be used to route intra-cluster traffic.
|
||||
// 2. Consider adding fields for ingress-type specific global options
|
||||
// usable by a loadbalancer, like http keep-alive.
|
||||
|
||||
HTTP *HTTPIngressRuleValue `json:"http,omitempty"`
|
||||
}
|
||||
|
||||
// HTTPIngressRuleValue is a list of http selectors pointing to backends.
|
||||
// In the example: http://<host>/<path>?<searchpart> -> backend where
|
||||
// where parts of the url correspond to RFC 3986, this resource will be used
|
||||
// to match against everything after the last '/' and before the first '?'
|
||||
// or '#'.
|
||||
type HTTPIngressRuleValue struct {
|
||||
// A collection of paths that map requests to backends.
|
||||
Paths []HTTPIngressPath `json:"paths"`
|
||||
// TODO: Consider adding fields for ingress-type specific global
|
||||
// options usable by a loadbalancer, like http keep-alive.
|
||||
}
|
||||
|
||||
// HTTPIngressPath associates a path regex with a backend. Incoming urls matching
|
||||
// the path are forwarded to the backend.
|
||||
type HTTPIngressPath struct {
|
||||
// Path is a extended POSIX regex as defined by IEEE Std 1003.1,
|
||||
// (i.e this follows the egrep/unix syntax, not the perl syntax)
|
||||
// matched against the path of an incoming request. Currently it can
|
||||
// contain characters disallowed from the conventional "path"
|
||||
// part of a URL as defined by RFC 3986. Paths must begin with
|
||||
// a '/'. If unspecified, the path defaults to a catch all sending
|
||||
// traffic to the backend.
|
||||
Path string `json:"path,omitempty"`
|
||||
|
||||
// Backend defines the referenced service endpoint to which the traffic
|
||||
// will be forwarded to.
|
||||
Backend IngressBackend `json:"backend"`
|
||||
}
|
||||
|
||||
// IngressBackend describes all endpoints for a given service and port.
|
||||
type IngressBackend struct {
|
||||
// Specifies the name of the referenced service.
|
||||
ServiceName string `json:"serviceName"`
|
||||
|
||||
// Specifies the port of the referenced service.
|
||||
ServicePort IntOrString `json:"servicePort"`
|
||||
}
|
32
provider/k8s/namespace.go
Normal file
32
provider/k8s/namespace.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Namespaces holds kubernetes namespaces
|
||||
type Namespaces []string
|
||||
|
||||
//Set adds strings elem into the the parser
|
||||
//it splits str on , and ;
|
||||
func (ns *Namespaces) Set(str string) error {
|
||||
fargs := func(c rune) bool {
|
||||
return c == ',' || c == ';'
|
||||
}
|
||||
// get function
|
||||
slice := strings.FieldsFunc(str, fargs)
|
||||
*ns = append(*ns, slice...)
|
||||
return nil
|
||||
}
|
||||
|
||||
//Get []string
|
||||
func (ns *Namespaces) Get() interface{} { return Namespaces(*ns) }
|
||||
|
||||
//String return slice in a string
|
||||
func (ns *Namespaces) String() string { return fmt.Sprintf("%v", *ns) }
|
||||
|
||||
//SetValue sets []string into the parser
|
||||
func (ns *Namespaces) SetValue(val interface{}) {
|
||||
*ns = Namespaces(val.(Namespaces))
|
||||
}
|
@@ -1,326 +0,0 @@
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TypeMeta describes an individual object in an API response or request
|
||||
// with strings representing the type of the object and its API schema version.
|
||||
// Structures that are versioned or persisted should inline TypeMeta.
|
||||
type TypeMeta struct {
|
||||
// Kind is a string value representing the REST resource this object represents.
|
||||
// Servers may infer this from the endpoint the client submits requests to.
|
||||
// Cannot be updated.
|
||||
// In CamelCase.
|
||||
// More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds
|
||||
Kind string `json:"kind,omitempty"`
|
||||
|
||||
// APIVersion defines the versioned schema of this representation of an object.
|
||||
// Servers should convert recognized schemas to the latest internal value, and
|
||||
// may reject unrecognized values.
|
||||
// More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#resources
|
||||
APIVersion string `json:"apiVersion,omitempty"`
|
||||
}
|
||||
|
||||
// ObjectMeta is metadata that all persisted resources must have, which includes all objects
|
||||
// users must create.
|
||||
type ObjectMeta struct {
|
||||
// Name is unique within a namespace. Name is required when creating resources, although
|
||||
// some resources may allow a client to request the generation of an appropriate name
|
||||
// automatically. Name is primarily intended for creation idempotence and configuration
|
||||
// definition.
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// GenerateName indicates that the name should be made unique by the server prior to persisting
|
||||
// it. A non-empty value for the field indicates the name will be made unique (and the name
|
||||
// returned to the client will be different than the name passed). The value of this field will
|
||||
// be combined with a unique suffix on the server if the Name field has not been provided.
|
||||
// The provided value must be valid within the rules for Name, and may be truncated by the length
|
||||
// of the suffix required to make the value unique on the server.
|
||||
//
|
||||
// If this field is specified, and Name is not present, the server will NOT return a 409 if the
|
||||
// generated name exists - instead, it will either return 201 Created or 500 with Reason
|
||||
// ServerTimeout indicating a unique name could not be found in the time allotted, and the client
|
||||
// should retry (optionally after the time indicated in the Retry-After header).
|
||||
GenerateName string `json:"generateName,omitempty"`
|
||||
|
||||
// Namespace defines the space within which name must be unique. An empty namespace is
|
||||
// equivalent to the "default" namespace, but "default" is the canonical representation.
|
||||
// Not all objects are required to be scoped to a namespace - the value of this field for
|
||||
// those objects will be empty.
|
||||
Namespace string `json:"namespace,omitempty"`
|
||||
|
||||
// SelfLink is a URL representing this object.
|
||||
SelfLink string `json:"selfLink,omitempty"`
|
||||
|
||||
// UID is the unique in time and space value for this object. It is typically generated by
|
||||
// the server on successful creation of a resource and is not allowed to change on PUT
|
||||
// operations.
|
||||
UID UID `json:"uid,omitempty"`
|
||||
|
||||
// An opaque value that represents the version of this resource. May be used for optimistic
|
||||
// concurrency, change detection, and the watch operation on a resource or set of resources.
|
||||
// Clients must treat these values as opaque and values may only be valid for a particular
|
||||
// resource or set of resources. Only servers will generate resource versions.
|
||||
ResourceVersion string `json:"resourceVersion,omitempty"`
|
||||
|
||||
// A sequence number representing a specific generation of the desired state.
|
||||
// Populated by the system. Read-only.
|
||||
Generation int64 `json:"generation,omitempty"`
|
||||
|
||||
// CreationTimestamp is a timestamp representing the server time when this object was
|
||||
// created. It is not guaranteed to be set in happens-before order across separate operations.
|
||||
// Clients may not set this value. It is represented in RFC3339 form and is in UTC.
|
||||
CreationTimestamp Time `json:"creationTimestamp,omitempty"`
|
||||
|
||||
// DeletionTimestamp is the time after which this resource will be deleted. This
|
||||
// field is set by the server when a graceful deletion is requested by the user, and is not
|
||||
// directly settable by a client. The resource will be deleted (no longer visible from
|
||||
// resource lists, and not reachable by name) after the time in this field. Once set, this
|
||||
// value may not be unset or be set further into the future, although it may be shortened
|
||||
// or the resource may be deleted prior to this time. For example, a user may request that
|
||||
// a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination
|
||||
// signal to the containers in the pod. Once the resource is deleted in the API, the Kubelet
|
||||
// will send a hard termination signal to the container.
|
||||
DeletionTimestamp *Time `json:"deletionTimestamp,omitempty"`
|
||||
|
||||
// DeletionGracePeriodSeconds records the graceful deletion value set when graceful deletion
|
||||
// was requested. Represents the most recent grace period, and may only be shortened once set.
|
||||
DeletionGracePeriodSeconds *int64 `json:"deletionGracePeriodSeconds,omitempty"`
|
||||
|
||||
// Labels are key value pairs that may be used to scope and select individual resources.
|
||||
// Label keys are of the form:
|
||||
// label-key ::= prefixed-name | name
|
||||
// prefixed-name ::= prefix '/' name
|
||||
// prefix ::= DNS_SUBDOMAIN
|
||||
// name ::= DNS_LABEL
|
||||
// The prefix is optional. If the prefix is not specified, the key is assumed to be private
|
||||
// to the user. Other system components that wish to use labels must specify a prefix. The
|
||||
// "kubernetes.io/" prefix is reserved for use by kubernetes components.
|
||||
// TODO: replace map[string]string with labels.LabelSet type
|
||||
Labels map[string]string `json:"labels,omitempty"`
|
||||
|
||||
// Annotations are unstructured key value data stored with a resource that may be set by
|
||||
// external tooling. They are not queryable and should be preserved when modifying
|
||||
// objects. Annotation keys have the same formatting restrictions as Label keys. See the
|
||||
// comments on Labels for details.
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
||||
// UID is a type that holds unique ID values, including UUIDs. Because we
|
||||
// don't ONLY use UUIDs, this is an alias to string. Being a type captures
|
||||
// intent and helps make sure that UIDs and names do not get conflated.
|
||||
type UID string
|
||||
|
||||
// Time is a wrapper around time.Time which supports correct
|
||||
// marshaling to YAML and JSON. Wrappers are provided for many
|
||||
// of the factory methods that the time package offers.
|
||||
//
|
||||
// +protobuf.options.marshal=false
|
||||
// +protobuf.as=Timestamp
|
||||
type Time struct {
|
||||
time.Time `protobuf:"-"`
|
||||
}
|
||||
|
||||
// Service is a named abstraction of software service (for example, mysql) consisting of local port
|
||||
// (for example 3306) that the proxy listens on, and the selector that determines which pods
|
||||
// will answer requests sent through the proxy.
|
||||
type Service struct {
|
||||
TypeMeta `json:",inline"`
|
||||
ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
// Spec defines the behavior of a service.
|
||||
Spec ServiceSpec `json:"spec,omitempty"`
|
||||
|
||||
// Status represents the current status of a service.
|
||||
Status ServiceStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceSpec describes the attributes that a user creates on a service
|
||||
type ServiceSpec struct {
|
||||
// Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer
|
||||
Type ServiceType `json:"type,omitempty"`
|
||||
|
||||
// Required: The list of ports that are exposed by this service.
|
||||
Ports []ServicePort `json:"ports"`
|
||||
|
||||
// This service will route traffic to pods having labels matching this selector. If empty or not present,
|
||||
// the service is assumed to have endpoints set by an external process and Kubernetes will not modify
|
||||
// those endpoints.
|
||||
Selector map[string]string `json:"selector"`
|
||||
|
||||
// ClusterIP is usually assigned by the master. If specified by the user
|
||||
// we will try to respect it or else fail the request. This field can
|
||||
// not be changed by updates.
|
||||
// Valid values are None, empty string (""), or a valid IP address
|
||||
// None can be specified for headless services when proxying is not required
|
||||
ClusterIP string `json:"clusterIP,omitempty"`
|
||||
|
||||
// ExternalIPs are used by external load balancers, or can be set by
|
||||
// users to handle external traffic that arrives at a node.
|
||||
ExternalIPs []string `json:"externalIPs,omitempty"`
|
||||
|
||||
// Only applies to Service Type: LoadBalancer
|
||||
// LoadBalancer will get created with the IP specified in this field.
|
||||
// This feature depends on whether the underlying cloud-provider supports specifying
|
||||
// the loadBalancerIP when a load balancer is created.
|
||||
// This field will be ignored if the cloud-provider does not support the feature.
|
||||
LoadBalancerIP string `json:"loadBalancerIP,omitempty"`
|
||||
|
||||
// Required: Supports "ClientIP" and "None". Used to maintain session affinity.
|
||||
SessionAffinity ServiceAffinity `json:"sessionAffinity,omitempty"`
|
||||
}
|
||||
|
||||
// ServicePort service port
|
||||
type ServicePort struct {
|
||||
// Optional if only one ServicePort is defined on this service: The
|
||||
// name of this port within the service. This must be a DNS_LABEL.
|
||||
// All ports within a ServiceSpec must have unique names. This maps to
|
||||
// the 'Name' field in EndpointPort objects.
|
||||
Name string `json:"name"`
|
||||
|
||||
// The IP protocol for this port. Supports "TCP" and "UDP".
|
||||
Protocol Protocol `json:"protocol"`
|
||||
|
||||
// The port that will be exposed on the service.
|
||||
Port int `json:"port"`
|
||||
|
||||
// Optional: The target port on pods selected by this service. If this
|
||||
// is a string, it will be looked up as a named port in the target
|
||||
// Pod's container ports. If this is not specified, the value
|
||||
// of the 'port' field is used (an identity map).
|
||||
// This field is ignored for services with clusterIP=None, and should be
|
||||
// omitted or set equal to the 'port' field.
|
||||
TargetPort IntOrString `json:"targetPort"`
|
||||
|
||||
// The port on each node on which this service is exposed.
|
||||
// Default is to auto-allocate a port if the ServiceType of this Service requires one.
|
||||
NodePort int `json:"nodePort"`
|
||||
}
|
||||
|
||||
// ServiceStatus represents the current status of a service
|
||||
type ServiceStatus struct {
|
||||
// LoadBalancer contains the current status of the load-balancer,
|
||||
// if one is present.
|
||||
LoadBalancer LoadBalancerStatus `json:"loadBalancer,omitempty"`
|
||||
}
|
||||
|
||||
// LoadBalancerStatus represents the status of a load-balancer
|
||||
type LoadBalancerStatus struct {
|
||||
// Ingress is a list containing ingress points for the load-balancer;
|
||||
// traffic intended for the service should be sent to these ingress points.
|
||||
Ingress []LoadBalancerIngress `json:"ingress,omitempty"`
|
||||
}
|
||||
|
||||
// LoadBalancerIngress represents the status of a load-balancer ingress point:
|
||||
// traffic intended for the service should be sent to an ingress point.
|
||||
type LoadBalancerIngress struct {
|
||||
// IP is set for load-balancer ingress points that are IP based
|
||||
// (typically GCE or OpenStack load-balancers)
|
||||
IP string `json:"ip,omitempty"`
|
||||
|
||||
// Hostname is set for load-balancer ingress points that are DNS based
|
||||
// (typically AWS load-balancers)
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
}
|
||||
|
||||
// ServiceAffinity Session Affinity Type string
|
||||
type ServiceAffinity string
|
||||
|
||||
// ServiceType Service Type string describes ingress methods for a service
|
||||
type ServiceType string
|
||||
|
||||
// Protocol defines network protocols supported for things like container ports.
|
||||
type Protocol string
|
||||
|
||||
// IntOrString is a type that can hold an int32 or a string. When used in
|
||||
// JSON or YAML marshalling and unmarshalling, it produces or consumes the
|
||||
// inner type. This allows you to have, for example, a JSON field that can
|
||||
// accept a name or number.
|
||||
// TODO: Rename to Int32OrString
|
||||
//
|
||||
// +protobuf=true
|
||||
// +protobuf.options.(gogoproto.goproto_stringer)=false
|
||||
type IntOrString struct {
|
||||
Type Type
|
||||
IntVal int32
|
||||
StrVal string
|
||||
}
|
||||
|
||||
// FromInt creates an IntOrString object with an int32 value. It is
|
||||
// your responsibility not to call this method with a value greater
|
||||
// than int32.
|
||||
// TODO: convert to (val int32)
|
||||
func FromInt(val int) IntOrString {
|
||||
return IntOrString{Type: Int, IntVal: int32(val)}
|
||||
}
|
||||
|
||||
// FromString creates an IntOrString object with a string value.
|
||||
func FromString(val string) IntOrString {
|
||||
return IntOrString{Type: String, StrVal: val}
|
||||
}
|
||||
|
||||
// String returns the string value, or the Itoa of the int value.
|
||||
func (intstr *IntOrString) String() string {
|
||||
if intstr.Type == String {
|
||||
return intstr.StrVal
|
||||
}
|
||||
return strconv.Itoa(intstr.IntValue())
|
||||
}
|
||||
|
||||
// IntValue returns the IntVal if type Int, or if
|
||||
// it is a String, will attempt a conversion to int.
|
||||
func (intstr *IntOrString) IntValue() int {
|
||||
if intstr.Type == String {
|
||||
i, _ := strconv.Atoi(intstr.StrVal)
|
||||
return i
|
||||
}
|
||||
return int(intstr.IntVal)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaller interface.
|
||||
func (intstr *IntOrString) UnmarshalJSON(value []byte) error {
|
||||
if value[0] == '"' {
|
||||
intstr.Type = String
|
||||
return json.Unmarshal(value, &intstr.StrVal)
|
||||
}
|
||||
intstr.Type = Int
|
||||
return json.Unmarshal(value, &intstr.IntVal)
|
||||
}
|
||||
|
||||
// Type represents the stored type of IntOrString.
|
||||
type Type int
|
||||
|
||||
const (
|
||||
// Int int
|
||||
Int Type = iota // The IntOrString holds an int.
|
||||
//String string
|
||||
String // The IntOrString holds a string.
|
||||
)
|
||||
|
||||
// ServiceList holds a list of services.
|
||||
type ServiceList struct {
|
||||
TypeMeta `json:",inline"`
|
||||
ListMeta `json:"metadata,omitempty"`
|
||||
|
||||
Items []Service `json:"items"`
|
||||
}
|
||||
|
||||
// ListMeta describes metadata that synthetic resources must have, including lists and
|
||||
// various status objects. A resource may have only one of {ObjectMeta, ListMeta}.
|
||||
type ListMeta struct {
|
||||
// SelfLink is a URL representing this object.
|
||||
// Populated by the system.
|
||||
// Read-only.
|
||||
SelfLink string `json:"selfLink,omitempty"`
|
||||
|
||||
// String that identifies the server's internal version of this object that
|
||||
// can be used by clients to determine when objects have changed.
|
||||
// Value must be treated as opaque by clients and passed unmodified back to the server.
|
||||
// Populated by the system.
|
||||
// Read-only.
|
||||
// More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#concurrency-control-and-consistency
|
||||
ResourceVersion string `json:"resourceVersion,omitempty"`
|
||||
}
|
@@ -1,13 +1,6 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/provider/k8s"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -16,86 +9,30 @@ import (
|
||||
|
||||
"github.com/cenk/backoff"
|
||||
"github.com/containous/traefik/job"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/provider/k8s"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"k8s.io/client-go/1.5/pkg/api/v1"
|
||||
"k8s.io/client-go/1.5/pkg/util/intstr"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceAccountToken = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||
serviceAccountCACert = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||
defaultKubeEndpoint = "http://127.0.0.1:8080"
|
||||
)
|
||||
|
||||
// Namespaces holds kubernetes namespaces
|
||||
type Namespaces []string
|
||||
|
||||
//Set adds strings elem into the the parser
|
||||
//it splits str on , and ;
|
||||
func (ns *Namespaces) Set(str string) error {
|
||||
fargs := func(c rune) bool {
|
||||
return c == ',' || c == ';'
|
||||
}
|
||||
// get function
|
||||
slice := strings.FieldsFunc(str, fargs)
|
||||
*ns = append(*ns, slice...)
|
||||
return nil
|
||||
}
|
||||
|
||||
//Get []string
|
||||
func (ns *Namespaces) Get() interface{} { return Namespaces(*ns) }
|
||||
|
||||
//String return slice in a string
|
||||
func (ns *Namespaces) String() string { return fmt.Sprintf("%v", *ns) }
|
||||
|
||||
//SetValue sets []string into the parser
|
||||
func (ns *Namespaces) SetValue(val interface{}) {
|
||||
*ns = Namespaces(val.(Namespaces))
|
||||
}
|
||||
|
||||
var _ Provider = (*Kubernetes)(nil)
|
||||
|
||||
// Kubernetes holds configurations of the Kubernetes provider.
|
||||
type Kubernetes struct {
|
||||
BaseProvider `mapstructure:",squash"`
|
||||
Endpoint string `description:"Kubernetes server endpoint"`
|
||||
DisablePassHostHeaders bool `description:"Kubernetes disable PassHost Headers"`
|
||||
Namespaces Namespaces `description:"Kubernetes namespaces"`
|
||||
LabelSelector string `description:"Kubernetes api label selector to use"`
|
||||
Endpoint string `description:"Kubernetes server endpoint"`
|
||||
DisablePassHostHeaders bool `description:"Kubernetes disable PassHost Headers"`
|
||||
Namespaces k8s.Namespaces `description:"Kubernetes namespaces"`
|
||||
LabelSelector string `description:"Kubernetes api label selector to use"`
|
||||
lastConfiguration safe.Safe
|
||||
}
|
||||
|
||||
func (provider *Kubernetes) createClient() (k8s.Client, error) {
|
||||
var token string
|
||||
tokenBytes, err := ioutil.ReadFile(serviceAccountToken)
|
||||
if err == nil {
|
||||
token = string(tokenBytes)
|
||||
log.Debugf("Kubernetes token: %s", token)
|
||||
} else {
|
||||
log.Errorf("Kubernetes load token error: %s", err)
|
||||
}
|
||||
caCert, err := ioutil.ReadFile(serviceAccountCACert)
|
||||
if err == nil {
|
||||
log.Debugf("Kubernetes CA cert: %s", serviceAccountCACert)
|
||||
} else {
|
||||
log.Errorf("Kubernetes load token error: %s", err)
|
||||
}
|
||||
kubernetesHost := os.Getenv("KUBERNETES_SERVICE_HOST")
|
||||
kubernetesPort := os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")
|
||||
// Prioritize user provided kubernetes endpoint since kube container runtime will almost always have it
|
||||
if provider.Endpoint == "" && len(kubernetesPort) > 0 && len(kubernetesHost) > 0 {
|
||||
log.Debugf("Using environment provided kubernetes endpoint")
|
||||
provider.Endpoint = "https://" + kubernetesHost + ":" + kubernetesPort
|
||||
}
|
||||
if provider.Endpoint == "" {
|
||||
log.Debugf("Using default kubernetes api endpoint")
|
||||
provider.Endpoint = defaultKubeEndpoint
|
||||
}
|
||||
log.Debugf("Kubernetes endpoint: %s", provider.Endpoint)
|
||||
return k8s.NewClient(provider.Endpoint, caCert, token)
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
|
||||
k8sClient, err := provider.createClient()
|
||||
k8sClient, err := k8s.NewClient(provider.Endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -104,10 +41,10 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage
|
||||
pool.Go(func(stop chan bool) {
|
||||
operation := func() error {
|
||||
for {
|
||||
stopWatch := make(chan bool, 5)
|
||||
stopWatch := make(chan struct{}, 1)
|
||||
defer close(stopWatch)
|
||||
log.Debugf("Using label selector: '%s'", provider.LabelSelector)
|
||||
eventsChan, errEventsChan, err := k8sClient.WatchAll(provider.LabelSelector, stopWatch)
|
||||
eventsChan, err := k8sClient.WatchAll(provider.LabelSelector, stopWatch)
|
||||
if err != nil {
|
||||
log.Errorf("Error watching kubernetes events: %v", err)
|
||||
timer := time.NewTimer(1 * time.Second)
|
||||
@@ -121,16 +58,11 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
stopWatch <- true
|
||||
return nil
|
||||
case err, _ := <-errEventsChan:
|
||||
stopWatch <- true
|
||||
return err
|
||||
case event := <-eventsChan:
|
||||
log.Debugf("Received event from kubernetes %+v", event)
|
||||
templateObjects, err := provider.loadIngresses(k8sClient)
|
||||
if err != nil {
|
||||
stopWatch <- true
|
||||
return err
|
||||
}
|
||||
if reflect.DeepEqual(provider.lastConfiguration.Get(), templateObjects) {
|
||||
@@ -150,45 +82,18 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Kubernetes connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot connect to Kubernetes server %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
templateObjects, err := provider.loadIngresses(k8sClient)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if reflect.DeepEqual(provider.lastConfiguration.Get(), templateObjects) {
|
||||
log.Debugf("Skipping configuration from kubernetes %+v", templateObjects)
|
||||
} else {
|
||||
provider.lastConfiguration.Set(templateObjects)
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "kubernetes",
|
||||
Configuration: provider.loadConfig(*templateObjects),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configuration, error) {
|
||||
ingresses, err := k8sClient.GetIngresses(provider.LabelSelector, func(ingress k8s.Ingress) bool {
|
||||
if len(provider.Namespaces) == 0 {
|
||||
return true
|
||||
}
|
||||
for _, n := range provider.Namespaces {
|
||||
if ingress.ObjectMeta.Namespace == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorf("Error retrieving ingresses: %+v", err)
|
||||
return nil, err
|
||||
}
|
||||
ingresses := k8sClient.GetIngresses(provider.Namespaces)
|
||||
|
||||
templateObjects := types.Configuration{
|
||||
map[string]*types.Backend{},
|
||||
map[string]*types.Frontend{},
|
||||
@@ -196,10 +101,18 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur
|
||||
PassHostHeader := provider.getPassHostHeader()
|
||||
for _, i := range ingresses {
|
||||
for _, r := range i.Spec.Rules {
|
||||
if r.HTTP == nil {
|
||||
log.Warnf("Error in ingress: HTTP is nil")
|
||||
continue
|
||||
}
|
||||
for _, pa := range r.HTTP.Paths {
|
||||
if _, exists := templateObjects.Backends[r.Host+pa.Path]; !exists {
|
||||
templateObjects.Backends[r.Host+pa.Path] = &types.Backend{
|
||||
Servers: make(map[string]types.Server),
|
||||
LoadBalancer: &types.LoadBalancer{
|
||||
Sticky: false,
|
||||
Method: "wrr",
|
||||
},
|
||||
}
|
||||
}
|
||||
if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
|
||||
@@ -211,9 +124,15 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur
|
||||
}
|
||||
}
|
||||
if len(r.Host) > 0 {
|
||||
rule := "Host:" + r.Host
|
||||
|
||||
if strings.Contains(r.Host, "*") {
|
||||
rule = "HostRegexp:" + strings.Replace(r.Host, "*", "{subdomain:[A-Za-z0-9-_]+}", 1)
|
||||
}
|
||||
|
||||
if _, exists := templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host]; !exists {
|
||||
templateObjects.Frontends[r.Host+pa.Path].Routes[r.Host] = types.Route{
|
||||
Rule: "Host:" + r.Host,
|
||||
Rule: rule,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,29 +159,53 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur
|
||||
Rule: ruleType + ":" + pa.Path,
|
||||
}
|
||||
}
|
||||
service, err := k8sClient.GetService(pa.Backend.ServiceName, i.ObjectMeta.Namespace)
|
||||
service, exists, err := k8sClient.GetService(i.ObjectMeta.Namespace, pa.Backend.ServiceName)
|
||||
if err != nil {
|
||||
log.Warnf("Error retrieving services: %v", err)
|
||||
log.Errorf("Error while retrieving service information from k8s API %s/%s: %v", i.ObjectMeta.Namespace, pa.Backend.ServiceName, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
log.Errorf("Service not found for %s/%s", i.ObjectMeta.Namespace, pa.Backend.ServiceName)
|
||||
delete(templateObjects.Frontends, r.Host+pa.Path)
|
||||
log.Warnf("Error retrieving services %s", pa.Backend.ServiceName)
|
||||
continue
|
||||
}
|
||||
|
||||
if expression := service.Annotations["traefik.backend.circuitbreaker"]; expression != "" {
|
||||
templateObjects.Backends[r.Host+pa.Path].CircuitBreaker = &types.CircuitBreaker{
|
||||
Expression: expression,
|
||||
}
|
||||
}
|
||||
if service.Annotations["traefik.backend.loadbalancer.method"] == "drr" {
|
||||
templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Method = "drr"
|
||||
}
|
||||
if service.Annotations["traefik.backend.loadbalancer.sticky"] == "true" {
|
||||
templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Sticky = true
|
||||
}
|
||||
|
||||
protocol := "http"
|
||||
for _, port := range service.Spec.Ports {
|
||||
if equalPorts(port, pa.Backend.ServicePort) {
|
||||
if port.Port == 443 {
|
||||
protocol = "https"
|
||||
}
|
||||
endpoints, err := k8sClient.GetEndpoints(service.ObjectMeta.Name, service.ObjectMeta.Namespace)
|
||||
|
||||
endpoints, exists, err := k8sClient.GetEndpoints(service.ObjectMeta.Namespace, service.ObjectMeta.Name)
|
||||
if err != nil {
|
||||
log.Errorf("Error retrieving endpoints: %v", err)
|
||||
log.Errorf("Error retrieving endpoints %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.Name, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
log.Errorf("Endpoints not found for %s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(endpoints.Subsets) == 0 {
|
||||
log.Warnf("Endpoints not found for %s/%s, falling back to Service ClusterIP", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
|
||||
log.Warnf("Service endpoints not found for %s/%s, falling back to Service ClusterIP", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
|
||||
templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{
|
||||
URL: protocol + "://" + service.Spec.ClusterIP + ":" + strconv.Itoa(port.Port),
|
||||
Weight: 0,
|
||||
URL: protocol + "://" + service.Spec.ClusterIP + ":" + strconv.Itoa(int(port.Port)),
|
||||
Weight: 1,
|
||||
}
|
||||
} else {
|
||||
for _, subset := range endpoints.Subsets {
|
||||
@@ -274,7 +217,7 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur
|
||||
}
|
||||
templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{
|
||||
URL: url,
|
||||
Weight: 0,
|
||||
Weight: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,7 +231,7 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur
|
||||
return &templateObjects, nil
|
||||
}
|
||||
|
||||
func endpointPortNumber(servicePort k8s.ServicePort, endpointPorts []k8s.EndpointPort) int {
|
||||
func endpointPortNumber(servicePort v1.ServicePort, endpointPorts []v1.EndpointPort) int {
|
||||
if len(endpointPorts) > 0 {
|
||||
//name is optional if there is only one port
|
||||
port := endpointPorts[0]
|
||||
@@ -299,11 +242,11 @@ func endpointPortNumber(servicePort k8s.ServicePort, endpointPorts []k8s.Endpoin
|
||||
}
|
||||
return int(port.Port)
|
||||
}
|
||||
return servicePort.Port
|
||||
return int(servicePort.Port)
|
||||
}
|
||||
|
||||
func equalPorts(servicePort k8s.ServicePort, ingressPort k8s.IntOrString) bool {
|
||||
if servicePort.Port == ingressPort.IntValue() {
|
||||
func equalPorts(servicePort v1.ServicePort, ingressPort intstr.IntOrString) bool {
|
||||
if int(servicePort.Port) == ingressPort.IntValue() {
|
||||
return true
|
||||
}
|
||||
if servicePort.Name != "" && servicePort.Name == ingressPort.String() {
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -2,12 +2,12 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"errors"
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
"github.com/cenk/backoff"
|
||||
"github.com/containous/traefik/job"
|
||||
@@ -76,7 +76,7 @@ func (provider *Kv) watchKv(configurationChan chan<- types.ConfigMessage, prefix
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("KV connection error: %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot connect to KV server: %v", err)
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage, pool *
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("KV connection error: %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Cannot connect to KV server: %v", err)
|
||||
}
|
||||
|
@@ -2,14 +2,14 @@ package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/containous/traefik/types"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func TestKvList(t *testing.T) {
|
||||
|
@@ -2,13 +2,13 @@ package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"math"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
@@ -25,13 +25,15 @@ var _ Provider = (*Marathon)(nil)
|
||||
// Marathon holds configuration of the Marathon provider.
|
||||
type Marathon struct {
|
||||
BaseProvider
|
||||
Endpoint string `description:"Marathon server endpoint. You can also specify multiple endpoint for Marathon"`
|
||||
Domain string `description:"Default domain used"`
|
||||
ExposedByDefault bool `description:"Expose Marathon apps by default"`
|
||||
GroupsAsSubDomains bool `description:"Convert Marathon groups to subdomains"`
|
||||
DCOSToken string `description:"DCOSToken for DCOS environment, This will override the Authorization header"`
|
||||
MarathonLBCompatibility bool `description:"Add compatibility with marathon-lb labels"`
|
||||
TLS *ClientTLS `description:"Enable Docker TLS support"`
|
||||
Endpoint string `description:"Marathon server endpoint. You can also specify multiple endpoint for Marathon"`
|
||||
Domain string `description:"Default domain used"`
|
||||
ExposedByDefault bool `description:"Expose Marathon apps by default"`
|
||||
GroupsAsSubDomains bool `description:"Convert Marathon groups to subdomains"`
|
||||
DCOSToken string `description:"DCOSToken for DCOS environment, This will override the Authorization header"`
|
||||
MarathonLBCompatibility bool `description:"Add compatibility with marathon-lb labels"`
|
||||
TLS *ClientTLS `description:"Enable Docker TLS support"`
|
||||
DialerTimeout time.Duration `description:"Set a non-default connection timeout for Marathon"`
|
||||
KeepAlive time.Duration `description:"Set a non-default TCP Keep Alive time in seconds"`
|
||||
Basic *MarathonBasic
|
||||
marathonClient marathon.Marathon
|
||||
}
|
||||
@@ -68,6 +70,10 @@ func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage,
|
||||
}
|
||||
config.HTTPClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{
|
||||
KeepAlive: provider.KeepAlive * time.Second,
|
||||
Timeout: time.Second * provider.DialerTimeout,
|
||||
}).DialContext,
|
||||
TLSClientConfig: TLSConfig,
|
||||
},
|
||||
}
|
||||
@@ -77,9 +83,10 @@ func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage,
|
||||
return err
|
||||
}
|
||||
provider.marathonClient = client
|
||||
update := make(marathon.EventsChannel, 5)
|
||||
|
||||
if provider.Watch {
|
||||
if err := client.AddEventsListener(update, marathon.EventIDApplications); err != nil {
|
||||
update, err := client.AddEventsListener(marathon.EventIDApplications)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to register for events, %s", err)
|
||||
return err
|
||||
}
|
||||
@@ -113,7 +120,7 @@ func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage,
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Marathon connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot connect to Marathon server %+v", err)
|
||||
}
|
||||
@@ -123,6 +130,7 @@ func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage,
|
||||
func (provider *Marathon) loadMarathonConfig() *types.Configuration {
|
||||
var MarathonFuncMap = template.FuncMap{
|
||||
"getBackend": provider.getBackend,
|
||||
"getBackendServer": provider.getBackendServer,
|
||||
"getPort": provider.getPort,
|
||||
"getWeight": provider.getWeight,
|
||||
"getDomain": provider.getDomain,
|
||||
@@ -132,7 +140,6 @@ func (provider *Marathon) loadMarathonConfig() *types.Configuration {
|
||||
"getEntryPoints": provider.getEntryPoints,
|
||||
"getFrontendRule": provider.getFrontendRule,
|
||||
"getFrontendBackend": provider.getFrontendBackend,
|
||||
"replace": replace,
|
||||
"hasCircuitBreakerLabels": provider.hasCircuitBreakerLabels,
|
||||
"hasLoadBalancerLabels": provider.hasLoadBalancerLabels,
|
||||
"hasMaxConnLabels": provider.hasMaxConnLabels,
|
||||
@@ -183,15 +190,16 @@ func (provider *Marathon) loadMarathonConfig() *types.Configuration {
|
||||
}
|
||||
|
||||
func (provider *Marathon) 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
|
||||
}
|
||||
ports := processPorts(application, task)
|
||||
if len(ports) == 0 {
|
||||
log.Debug("Filtering marathon task without port %s", task.AppID)
|
||||
return false
|
||||
}
|
||||
label, _ := provider.getLabel(application, "traefik.tags")
|
||||
constraintTags := strings.Split(label, ",")
|
||||
if provider.MarathonLBCompatibility {
|
||||
@@ -218,10 +226,9 @@ func (provider *Marathon) taskFilter(task marathon.Task, applications *marathon.
|
||||
log.Debugf("Filtering marathon task %s specifying both traefik.portIndex and traefik.port labels", task.AppID)
|
||||
return false
|
||||
}
|
||||
|
||||
if portIndexLabel != "" {
|
||||
index, err := strconv.Atoi((*application.Labels)["traefik.portIndex"])
|
||||
if err != nil || index < 0 || index > len(application.Ports)-1 {
|
||||
if err != nil || index < 0 || index > len(ports)-1 {
|
||||
log.Debugf("Filtering marathon task %s with unexpected value for traefik.portIndex label", task.AppID)
|
||||
return false
|
||||
}
|
||||
@@ -234,7 +241,7 @@ func (provider *Marathon) taskFilter(task marathon.Task, applications *marathon.
|
||||
}
|
||||
|
||||
var foundPort bool
|
||||
for _, exposedPort := range task.Ports {
|
||||
for _, exposedPort := range ports {
|
||||
if port == exposedPort {
|
||||
foundPort = true
|
||||
break
|
||||
@@ -310,17 +317,17 @@ func (provider *Marathon) getPort(task marathon.Task, applications []marathon.Ap
|
||||
log.Errorf("Unable to get marathon application from task %s", task.AppID)
|
||||
return ""
|
||||
}
|
||||
|
||||
ports := processPorts(application, task)
|
||||
if portIndexLabel, err := provider.getLabel(application, "traefik.portIndex"); err == nil {
|
||||
if index, err := strconv.Atoi(portIndexLabel); err == nil {
|
||||
return strconv.Itoa(task.Ports[index])
|
||||
return strconv.Itoa(ports[index])
|
||||
}
|
||||
}
|
||||
if portValueLabel, err := provider.getLabel(application, "traefik.port"); err == nil {
|
||||
return portValueLabel
|
||||
}
|
||||
|
||||
for _, port := range task.Ports {
|
||||
for _, port := range ports {
|
||||
return strconv.Itoa(port)
|
||||
}
|
||||
return ""
|
||||
@@ -483,3 +490,57 @@ func (provider *Marathon) getCircuitBreakerExpression(application marathon.Appli
|
||||
}
|
||||
return "NetworkErrorRatio() > 1"
|
||||
}
|
||||
|
||||
func processPorts(application marathon.Application, task marathon.Task) []int {
|
||||
|
||||
// Using default port configuration
|
||||
if task.Ports != nil && len(task.Ports) > 0 {
|
||||
return task.Ports
|
||||
}
|
||||
|
||||
// Using port definition if available
|
||||
if application.PortDefinitions != nil && len(*application.PortDefinitions) > 0 {
|
||||
var ports []int
|
||||
for _, def := range *application.PortDefinitions {
|
||||
if def.Port != nil {
|
||||
ports = append(ports, *def.Port)
|
||||
}
|
||||
}
|
||||
return ports
|
||||
}
|
||||
// If using IP-per-task using this port definition
|
||||
if application.IPAddressPerTask != nil && len(*((*application.IPAddressPerTask).Discovery).Ports) > 0 {
|
||||
var ports []int
|
||||
for _, def := range *((*application.IPAddressPerTask).Discovery).Ports {
|
||||
ports = append(ports, def.Number)
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
return []int{}
|
||||
}
|
||||
|
||||
func (provider *Marathon) getBackendServer(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 len(task.IPAddresses) == 0 {
|
||||
return ""
|
||||
} else if len(task.IPAddresses) == 1 {
|
||||
return task.IPAddresses[0].IPAddress
|
||||
} else {
|
||||
ipAddressIdxStr, err := provider.getLabel(application, "traefik.ipAddressIdx")
|
||||
if err != nil {
|
||||
log.Errorf("Unable to get marathon IPAddress from task %s", task.AppID)
|
||||
return ""
|
||||
}
|
||||
ipAddressIdx, err := strconv.Atoi(ipAddressIdxStr)
|
||||
if err != nil {
|
||||
log.Errorf("Invalid marathon IPAddress from task %s", task.AppID)
|
||||
return ""
|
||||
}
|
||||
return task.IPAddresses[ipAddressIdx].IPAddress
|
||||
}
|
||||
}
|
||||
|
@@ -1,11 +1,10 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/containous/traefik/mocks"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/gambol99/go-marathon"
|
||||
@@ -81,8 +80,14 @@ func TestMarathonLoadConfig(t *testing.T) {
|
||||
{
|
||||
ID: "test",
|
||||
AppID: "/test",
|
||||
Host: "127.0.0.1",
|
||||
Host: "localhost",
|
||||
Ports: []int{80},
|
||||
IPAddresses: []*marathon.IPAddress{
|
||||
{
|
||||
IPAddress: "127.0.0.1",
|
||||
Protocol: "tcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -128,8 +133,14 @@ func TestMarathonLoadConfig(t *testing.T) {
|
||||
{
|
||||
ID: "testLoadBalancerAndCircuitBreaker.dot",
|
||||
AppID: "/testLoadBalancerAndCircuitBreaker.dot",
|
||||
Host: "127.0.0.1",
|
||||
Host: "localhost",
|
||||
Ports: []int{80},
|
||||
IPAddresses: []*marathon.IPAddress{
|
||||
{
|
||||
IPAddress: "127.0.0.1",
|
||||
Protocol: "tcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -180,8 +191,14 @@ func TestMarathonLoadConfig(t *testing.T) {
|
||||
{
|
||||
ID: "testMaxConn",
|
||||
AppID: "/testMaxConn",
|
||||
Host: "127.0.0.1",
|
||||
Host: "localhost",
|
||||
Ports: []int{80},
|
||||
IPAddresses: []*marathon.IPAddress{
|
||||
{
|
||||
IPAddress: "127.0.0.1",
|
||||
Protocol: "tcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -229,8 +246,14 @@ func TestMarathonLoadConfig(t *testing.T) {
|
||||
{
|
||||
ID: "testMaxConnOnlySpecifyAmount",
|
||||
AppID: "/testMaxConnOnlySpecifyAmount",
|
||||
Host: "127.0.0.1",
|
||||
Host: "localhost",
|
||||
Ports: []int{80},
|
||||
IPAddresses: []*marathon.IPAddress{
|
||||
{
|
||||
IPAddress: "127.0.0.1",
|
||||
Protocol: "tcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -275,8 +298,14 @@ func TestMarathonLoadConfig(t *testing.T) {
|
||||
{
|
||||
ID: "testMaxConnOnlyExtractorFunc",
|
||||
AppID: "/testMaxConnOnlyExtractorFunc",
|
||||
Host: "127.0.0.1",
|
||||
Host: "localhost",
|
||||
Ports: []int{80},
|
||||
IPAddresses: []*marathon.IPAddress{
|
||||
{
|
||||
IPAddress: "127.0.0.1",
|
||||
Protocol: "tcp",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -388,7 +417,89 @@ func TestMarathonTaskFilter(t *testing.T) {
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "disable",
|
||||
AppID: "ipAddressOnePort",
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "ipAddressOnePort",
|
||||
IPAddressPerTask: &marathon.IPAddressPerTask{
|
||||
Discovery: &marathon.Discovery{
|
||||
Ports: &[]marathon.Port{
|
||||
{
|
||||
Number: 8880,
|
||||
Name: "p1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Labels: &map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "ipAddressTwoPortsUseFirst",
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "ipAddressTwoPortsUseFirst",
|
||||
IPAddressPerTask: &marathon.IPAddressPerTask{
|
||||
Discovery: &marathon.Discovery{
|
||||
Ports: &[]marathon.Port{
|
||||
{
|
||||
Number: 8898,
|
||||
Name: "p1",
|
||||
}, {
|
||||
Number: 9999,
|
||||
Name: "p1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Labels: &map[string]string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
AppID: "ipAddressValidTwoPorts",
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
Apps: []marathon.Application{
|
||||
{
|
||||
ID: "ipAddressValidTwoPorts",
|
||||
IPAddressPerTask: &marathon.IPAddressPerTask{
|
||||
Discovery: &marathon.Discovery{
|
||||
Ports: &[]marathon.Port{
|
||||
{
|
||||
Number: 8898,
|
||||
Name: "p1",
|
||||
}, {
|
||||
Number: 9999,
|
||||
Name: "p2",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Labels: &map[string]string{
|
||||
"traefik.portIndex": "0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: true,
|
||||
exposedByDefault: true,
|
||||
},
|
||||
{
|
||||
task: marathon.Task{
|
||||
Ports: []int{80},
|
||||
},
|
||||
applications: &marathon.Applications{
|
||||
@@ -966,6 +1077,19 @@ func TestMarathonGetPort(t *testing.T) {
|
||||
Ports: []int{80, 443},
|
||||
},
|
||||
expected: "443",
|
||||
}, {
|
||||
applications: []marathon.Application{
|
||||
{
|
||||
ID: "application-with-port",
|
||||
Ports: []int{9999},
|
||||
Labels: &map[string]string{},
|
||||
},
|
||||
},
|
||||
task: marathon.Task{
|
||||
AppID: "application-with-port",
|
||||
Ports: []int{7777},
|
||||
},
|
||||
expected: "7777",
|
||||
},
|
||||
}
|
||||
|
||||
|
@@ -2,11 +2,10 @@ package provider
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
@@ -16,7 +15,8 @@ import (
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/mesos/mesos-go/detector"
|
||||
_ "github.com/mesos/mesos-go/detector/zoo" // Registers the ZK detector
|
||||
// Register mesos zoo the detector
|
||||
_ "github.com/mesos/mesos-go/detector/zoo"
|
||||
"github.com/mesosphere/mesos-dns/detect"
|
||||
"github.com/mesosphere/mesos-dns/logging"
|
||||
"github.com/mesosphere/mesos-dns/records"
|
||||
@@ -113,7 +113,7 @@ func (provider *Mesos) Provide(configurationChan chan<- types.ConfigMessage, poo
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("mesos connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot connect to mesos server %+v", err)
|
||||
}
|
||||
@@ -135,7 +135,6 @@ func (provider *Mesos) loadMesosConfig() *types.Configuration {
|
||||
"getFrontendBackend": provider.getFrontendBackend,
|
||||
"getID": provider.getID,
|
||||
"getFrontEndName": provider.getFrontEndName,
|
||||
"replace": replace,
|
||||
}
|
||||
|
||||
t := records.NewRecordGenerator(time.Duration(provider.StateTimeoutSecond) * time.Second)
|
||||
|
@@ -2,15 +2,14 @@ package provider
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"unicode"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/containous/traefik/autogen"
|
||||
@@ -58,7 +57,19 @@ func (p *BaseProvider) getConfiguration(defaultTemplateFile string, funcMap temp
|
||||
err error
|
||||
)
|
||||
configuration := new(types.Configuration)
|
||||
tmpl := template.New(p.Filename).Funcs(funcMap)
|
||||
var defaultFuncMap = template.FuncMap{
|
||||
"replace": replace,
|
||||
"tolower": strings.ToLower,
|
||||
"normalize": normalize,
|
||||
"split": split,
|
||||
"contains": contains,
|
||||
}
|
||||
|
||||
for funcID, funcElement := range funcMap {
|
||||
defaultFuncMap[funcID] = funcElement
|
||||
}
|
||||
|
||||
tmpl := template.New(p.Filename).Funcs(defaultFuncMap)
|
||||
if len(p.Filename) > 0 {
|
||||
buf, err = ioutil.ReadFile(p.Filename)
|
||||
if err != nil {
|
||||
@@ -93,6 +104,14 @@ func replace(s1 string, s2 string, s3 string) string {
|
||||
return strings.Replace(s3, s1, s2, -1)
|
||||
}
|
||||
|
||||
func contains(substr, s string) bool {
|
||||
return strings.Contains(s, substr)
|
||||
}
|
||||
|
||||
func split(sep, s string) []string {
|
||||
return strings.Split(s, sep)
|
||||
}
|
||||
|
||||
func normalize(name string) string {
|
||||
fargs := func(c rune) bool {
|
||||
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
|
||||
|
@@ -325,3 +325,58 @@ func TestMatchingConstraints(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultFuncMap(t *testing.T) {
|
||||
templateFile, err := ioutil.TempFile("", "provider-configuration")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(templateFile.Name())
|
||||
data := []byte(`
|
||||
[backends]
|
||||
[backends.{{ "backend-1" | replace "-" "" }}]
|
||||
[backends.{{ "BACKEND1" | tolower }}.circuitbreaker]
|
||||
expression = "NetworkErrorRatio() > 0.5"
|
||||
[backends.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.{{normalize "frontend/1"}}]
|
||||
{{ $backend := "backend1/test/value" | split "/" }}
|
||||
{{ $backendid := index $backend 1 }}
|
||||
{{ if "backend1" | contains "backend" }}
|
||||
backend = "backend1"
|
||||
{{end}}
|
||||
passHostHeader = true
|
||||
[frontends.frontend-1.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(),
|
||||
},
|
||||
nil,
|
||||
}
|
||||
configuration, err := provider.getConfiguration(templateFile.Name(), nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Shouldn't have error out, got %v", err)
|
||||
}
|
||||
if configuration == nil {
|
||||
t.Fatalf("Configuration should not be nil, but was")
|
||||
}
|
||||
if _, ok := configuration.Backends["backend1"]; !ok {
|
||||
t.Fatalf("backend1 should exists, but it not")
|
||||
}
|
||||
if _, ok := configuration.Frontends["frontend-1"]; !ok {
|
||||
t.Fatalf("Frontend frontend-1 should exists, but it not")
|
||||
}
|
||||
}
|
||||
|
474
provider/rancher.go
Normal file
474
provider/rancher.go
Normal file
@@ -0,0 +1,474 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/ty/fun"
|
||||
"github.com/cenk/backoff"
|
||||
"github.com/containous/traefik/job"
|
||||
"github.com/containous/traefik/log"
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
rancher "github.com/rancher/go-rancher/client"
|
||||
)
|
||||
|
||||
const (
|
||||
// RancherDefaultWatchTime is the duration of the interval when polling rancher
|
||||
RancherDefaultWatchTime = 15 * time.Second
|
||||
)
|
||||
|
||||
var _ Provider = (*Rancher)(nil)
|
||||
|
||||
// Rancher holds configurations of the Rancher provider.
|
||||
type Rancher struct {
|
||||
BaseProvider `mapstructure:",squash"`
|
||||
Endpoint string `description:"Rancher server HTTP(S) endpoint."`
|
||||
AccessKey string `description:"Rancher server access key."`
|
||||
SecretKey string `description:"Rancher server Secret Key."`
|
||||
ExposedByDefault bool `description:"Expose Services by default"`
|
||||
Domain string `description:"Default domain used"`
|
||||
}
|
||||
|
||||
type rancherData struct {
|
||||
Name string
|
||||
Labels map[string]string // List of labels set to container or service
|
||||
Containers []string
|
||||
Health string
|
||||
}
|
||||
|
||||
func (r rancherData) String() string {
|
||||
return fmt.Sprintf("{name:%s, labels:%v, containers: %v, health: %s}", r.Name, r.Labels, r.Containers, r.Health)
|
||||
}
|
||||
|
||||
// Frontend Labels
|
||||
func (provider *Rancher) getPassHostHeader(service rancherData) string {
|
||||
if passHostHeader, err := getServiceLabel(service, "traefik.frontend.passHostHeader"); err == nil {
|
||||
return passHostHeader
|
||||
}
|
||||
return "true"
|
||||
}
|
||||
|
||||
func (provider *Rancher) getPriority(service rancherData) string {
|
||||
if priority, err := getServiceLabel(service, "traefik.frontend.priority"); err == nil {
|
||||
return priority
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func (provider *Rancher) getEntryPoints(service rancherData) []string {
|
||||
if entryPoints, err := getServiceLabel(service, "traefik.frontend.entryPoints"); err == nil {
|
||||
return strings.Split(entryPoints, ",")
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (provider *Rancher) getFrontendRule(service rancherData) string {
|
||||
if label, err := getServiceLabel(service, "traefik.frontend.rule"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "Host:" + strings.ToLower(strings.Replace(service.Name, "/", ".", -1)) + "." + provider.Domain
|
||||
}
|
||||
|
||||
func (provider *Rancher) getFrontendName(service rancherData) string {
|
||||
// Replace '.' with '-' in quoted keys because of this issue https://github.com/BurntSushi/toml/issues/78
|
||||
return normalize(provider.getFrontendRule(service))
|
||||
}
|
||||
|
||||
// Backend Labels
|
||||
func (provider *Rancher) getLoadBalancerMethod(service rancherData) string {
|
||||
if label, err := getServiceLabel(service, "traefik.backend.loadbalancer.method"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "wrr"
|
||||
}
|
||||
|
||||
func (provider *Rancher) hasLoadBalancerLabel(service rancherData) bool {
|
||||
_, errMethod := getServiceLabel(service, "traefik.backend.loadbalancer.method")
|
||||
_, errSticky := getServiceLabel(service, "traefik.backend.loadbalancer.sticky")
|
||||
if errMethod != nil && errSticky != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (provider *Rancher) hasCircuitBreakerLabel(service rancherData) bool {
|
||||
if _, err := getServiceLabel(service, "traefik.backend.circuitbreaker.expression"); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (provider *Rancher) getCircuitBreakerExpression(service rancherData) string {
|
||||
if label, err := getServiceLabel(service, "traefik.backend.circuitbreaker.expression"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "NetworkErrorRatio() > 1"
|
||||
}
|
||||
|
||||
func (provider *Rancher) getSticky(service rancherData) string {
|
||||
if _, err := getServiceLabel(service, "traefik.backend.loadbalancer.sticky"); err == nil {
|
||||
return "true"
|
||||
}
|
||||
return "false"
|
||||
}
|
||||
|
||||
func (provider *Rancher) getBackend(service rancherData) string {
|
||||
if label, err := getServiceLabel(service, "traefik.backend"); err == nil {
|
||||
return normalize(label)
|
||||
}
|
||||
return normalize(service.Name)
|
||||
}
|
||||
|
||||
// Generall Application Stuff
|
||||
func (provider *Rancher) getPort(service rancherData) string {
|
||||
if label, err := getServiceLabel(service, "traefik.port"); err == nil {
|
||||
return label
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (provider *Rancher) getProtocol(service rancherData) string {
|
||||
if label, err := getServiceLabel(service, "traefik.protocol"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
func (provider *Rancher) getWeight(service rancherData) string {
|
||||
if label, err := getServiceLabel(service, "traefik.weight"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "0"
|
||||
}
|
||||
|
||||
func (provider *Rancher) getDomain(service rancherData) string {
|
||||
if label, err := getServiceLabel(service, "traefik.domain"); err == nil {
|
||||
return label
|
||||
}
|
||||
return provider.Domain
|
||||
}
|
||||
|
||||
func (provider *Rancher) hasMaxConnLabels(service rancherData) bool {
|
||||
if _, err := getServiceLabel(service, "traefik.backend.maxconn.amount"); err != nil {
|
||||
return false
|
||||
}
|
||||
if _, err := getServiceLabel(service, "traefik.backend.maxconn.extractorfunc"); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (provider *Rancher) getMaxConnAmount(service rancherData) int64 {
|
||||
if label, err := getServiceLabel(service, "traefik.backend.maxconn.amount"); err == nil {
|
||||
i, errConv := strconv.ParseInt(label, 10, 64)
|
||||
if errConv != nil {
|
||||
log.Errorf("Unable to parse traefik.backend.maxconn.amount %s", label)
|
||||
return math.MaxInt64
|
||||
}
|
||||
return i
|
||||
}
|
||||
return math.MaxInt64
|
||||
}
|
||||
|
||||
func (provider *Rancher) getMaxConnExtractorFunc(service rancherData) string {
|
||||
if label, err := getServiceLabel(service, "traefik.backend.maxconn.extractorfunc"); err == nil {
|
||||
return label
|
||||
}
|
||||
return "request.host"
|
||||
}
|
||||
|
||||
func getServiceLabel(service rancherData, label string) (string, error) {
|
||||
for key, value := range service.Labels {
|
||||
if key == label {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("Label not found:" + label)
|
||||
}
|
||||
|
||||
func (provider *Rancher) createClient() (*rancher.RancherClient, error) {
|
||||
|
||||
rancherURL := getenv("CATTLE_URL", provider.Endpoint)
|
||||
accessKey := getenv("CATTLE_ACCESS_KEY", provider.AccessKey)
|
||||
secretKey := getenv("CATTLE_SECRET_KEY", provider.SecretKey)
|
||||
|
||||
return rancher.NewRancherClient(&rancher.ClientOpts{
|
||||
Url: rancherURL,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
})
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
value := os.Getenv(key)
|
||||
if len(value) == 0 {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Provide allows the provider to provide configurations to traefik
|
||||
// using the given configuration channel.
|
||||
func (provider *Rancher) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
|
||||
|
||||
safe.Go(func() {
|
||||
operation := func() error {
|
||||
rancherClient, err := provider.createClient()
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create a client for rancher, error: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
var environments = listRancherEnvironments(rancherClient)
|
||||
var services = listRancherServices(rancherClient)
|
||||
var container = listRancherContainer(rancherClient)
|
||||
|
||||
var rancherData = parseRancherData(environments, services, container)
|
||||
|
||||
configuration := provider.loadRancherConfig(rancherData)
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "rancher",
|
||||
Configuration: configuration,
|
||||
}
|
||||
|
||||
if provider.Watch {
|
||||
_, cancel := context.WithCancel(ctx)
|
||||
ticker := time.NewTicker(RancherDefaultWatchTime)
|
||||
pool.Go(func(stop chan bool) {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
|
||||
log.Debugf("Refreshing new Data from Rancher API")
|
||||
var environments = listRancherEnvironments(rancherClient)
|
||||
var services = listRancherServices(rancherClient)
|
||||
var container = listRancherContainer(rancherClient)
|
||||
|
||||
rancherData := parseRancherData(environments, services, container)
|
||||
|
||||
configuration := provider.loadRancherConfig(rancherData)
|
||||
if configuration != nil {
|
||||
configurationChan <- types.ConfigMessage{
|
||||
ProviderName: "rancher",
|
||||
Configuration: configuration,
|
||||
}
|
||||
}
|
||||
case <-stop:
|
||||
ticker.Stop()
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
notify := func(err error, time time.Duration) {
|
||||
log.Errorf("Rancher connection error %+v, retrying in %s", err, time)
|
||||
}
|
||||
err := backoff.RetryNotify(operation, job.NewBackOff(backoff.NewExponentialBackOff()), notify)
|
||||
if err != nil {
|
||||
log.Errorf("Cannot connect to Rancher Endpoint %+v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listRancherEnvironments(client *rancher.RancherClient) []*rancher.Environment {
|
||||
|
||||
var environmentList = []*rancher.Environment{}
|
||||
|
||||
environments, err := client.Environment.List(nil)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Cannot get Rancher Environments %+v", err)
|
||||
}
|
||||
|
||||
for k := range environments.Data {
|
||||
environmentList = append(environmentList, &environments.Data[k])
|
||||
}
|
||||
|
||||
return environmentList
|
||||
}
|
||||
|
||||
func listRancherServices(client *rancher.RancherClient) []*rancher.Service {
|
||||
|
||||
var servicesList = []*rancher.Service{}
|
||||
|
||||
services, err := client.Service.List(nil)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Cannot get Rancher Services %+v", err)
|
||||
}
|
||||
|
||||
for k := range services.Data {
|
||||
servicesList = append(servicesList, &services.Data[k])
|
||||
}
|
||||
|
||||
return servicesList
|
||||
}
|
||||
|
||||
func listRancherContainer(client *rancher.RancherClient) []*rancher.Container {
|
||||
|
||||
containerList := []*rancher.Container{}
|
||||
|
||||
container, err := client.Container.List(nil)
|
||||
|
||||
log.Debugf("first container len: %i", len(container.Data))
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("Cannot get Rancher Services %+v", err)
|
||||
}
|
||||
|
||||
valid := true
|
||||
|
||||
for valid {
|
||||
for k := range container.Data {
|
||||
containerList = append(containerList, &container.Data[k])
|
||||
}
|
||||
|
||||
container, err = container.Next()
|
||||
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if container == nil || len(container.Data) == 0 {
|
||||
valid = false
|
||||
}
|
||||
}
|
||||
|
||||
return containerList
|
||||
}
|
||||
|
||||
func parseRancherData(environments []*rancher.Environment, services []*rancher.Service, containers []*rancher.Container) []rancherData {
|
||||
var rancherDataList []rancherData
|
||||
|
||||
for _, environment := range environments {
|
||||
|
||||
for _, service := range services {
|
||||
if service.EnvironmentId != environment.Id {
|
||||
continue
|
||||
}
|
||||
|
||||
rancherData := rancherData{
|
||||
Name: environment.Name + "/" + service.Name,
|
||||
Health: service.HealthState,
|
||||
Labels: make(map[string]string),
|
||||
Containers: []string{},
|
||||
}
|
||||
|
||||
for key, value := range service.LaunchConfig.Labels {
|
||||
rancherData.Labels[key] = value.(string)
|
||||
}
|
||||
|
||||
for _, container := range containers {
|
||||
for key, value := range container.Labels {
|
||||
|
||||
if key == "io.rancher.stack_service.name" && value == rancherData.Name {
|
||||
rancherData.Containers = append(rancherData.Containers, container.PrimaryIpAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
rancherDataList = append(rancherDataList, rancherData)
|
||||
}
|
||||
}
|
||||
|
||||
return rancherDataList
|
||||
}
|
||||
|
||||
func (provider *Rancher) loadRancherConfig(services []rancherData) *types.Configuration {
|
||||
|
||||
var RancherFuncMap = template.FuncMap{
|
||||
"getPort": provider.getPort,
|
||||
"getBackend": provider.getBackend,
|
||||
"getWeight": provider.getWeight,
|
||||
"getDomain": provider.getDomain,
|
||||
"getProtocol": provider.getProtocol,
|
||||
"getPassHostHeader": provider.getPassHostHeader,
|
||||
"getPriority": provider.getPriority,
|
||||
"getEntryPoints": provider.getEntryPoints,
|
||||
"getFrontendRule": provider.getFrontendRule,
|
||||
"hasCircuitBreakerLabel": provider.hasCircuitBreakerLabel,
|
||||
"getCircuitBreakerExpression": provider.getCircuitBreakerExpression,
|
||||
"hasLoadBalancerLabel": provider.hasLoadBalancerLabel,
|
||||
"getLoadBalancerMethod": provider.getLoadBalancerMethod,
|
||||
"hasMaxConnLabels": provider.hasMaxConnLabels,
|
||||
"getMaxConnAmount": provider.getMaxConnAmount,
|
||||
"getMaxConnExtractorFunc": provider.getMaxConnExtractorFunc,
|
||||
"getSticky": provider.getSticky,
|
||||
}
|
||||
|
||||
// filter services
|
||||
filteredServices := fun.Filter(func(service rancherData) bool {
|
||||
return provider.serviceFilter(service)
|
||||
}, services).([]rancherData)
|
||||
|
||||
frontends := map[string]rancherData{}
|
||||
backends := map[string]rancherData{}
|
||||
|
||||
for _, service := range filteredServices {
|
||||
frontendName := provider.getFrontendName(service)
|
||||
frontends[frontendName] = service
|
||||
backendName := provider.getBackend(service)
|
||||
backends[backendName] = service
|
||||
}
|
||||
|
||||
templateObjects := struct {
|
||||
Frontends map[string]rancherData
|
||||
Backends map[string]rancherData
|
||||
Domain string
|
||||
}{
|
||||
frontends,
|
||||
backends,
|
||||
provider.Domain,
|
||||
}
|
||||
|
||||
configuration, err := provider.getConfiguration("templates/rancher.tmpl", RancherFuncMap, templateObjects)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
return configuration
|
||||
|
||||
}
|
||||
|
||||
func (provider *Rancher) serviceFilter(service rancherData) bool {
|
||||
|
||||
if service.Labels["traefik.port"] == "" {
|
||||
log.Debugf("Filtering service %s without traefik.port label", service.Name)
|
||||
return false
|
||||
}
|
||||
|
||||
if !isServiceEnabled(service, provider.ExposedByDefault) {
|
||||
log.Debugf("Filtering disabled service %s", service.Name)
|
||||
return false
|
||||
}
|
||||
|
||||
if service.Health != "" && service.Health != "healthy" {
|
||||
log.Debugf("Filtering unhealthy or starting service %s", service.Name)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isServiceEnabled(service rancherData, exposedByDefault bool) bool {
|
||||
|
||||
if service.Labels["traefik.enable"] != "" {
|
||||
var v = service.Labels["traefik.enable"]
|
||||
return exposedByDefault && v != "false" || v == "true"
|
||||
}
|
||||
return exposedByDefault
|
||||
}
|
454
provider/rancher_test.go
Normal file
454
provider/rancher_test.go
Normal file
@@ -0,0 +1,454 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/containous/traefik/types"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRancherGetFrontendName(t *testing.T) {
|
||||
provider := &Rancher{
|
||||
Domain: "rancher.localhost",
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
service rancherData
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "foo",
|
||||
},
|
||||
expected: "Host-foo-rancher-localhost",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Headers:User-Agent,bat/0.1.0",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "Headers-User-Agent-bat-0-1-0",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Host:foo.bar",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "Host-foo-bar",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Path:/test",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "Path-test",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "PathPrefix:/test2",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "PathPrefix-test2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
actual := provider.getFrontendName(e.service)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRancherGetFrontendRule(t *testing.T) {
|
||||
provider := &Rancher{
|
||||
Domain: "rancher.localhost",
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
service rancherData
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "foo",
|
||||
},
|
||||
expected: "Host:foo.rancher.localhost",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "foo/bar",
|
||||
},
|
||||
expected: "Host:foo.bar.rancher.localhost",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Host:foo.bar.com",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "Host:foo.bar.com",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "Path:/test",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "Path:/test",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.rule": "PathPrefix:/test2",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "PathPrefix:/test2",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
actual := provider.getFrontendRule(e.service)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRancherGetBackend(t *testing.T) {
|
||||
provider := &Rancher{
|
||||
Domain: "rancher.localhost",
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
service rancherData
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
},
|
||||
expected: "test-service",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.backend": "foobar",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "foobar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
actual := provider.getBackend(e.service)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRancherGetWeight(t *testing.T) {
|
||||
provider := &Rancher{
|
||||
Domain: "rancher.localhost",
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
service rancherData
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
},
|
||||
expected: "0",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.weight": "5",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "5",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
actual := provider.getWeight(e.service)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRancherGetPort(t *testing.T) {
|
||||
provider := &Rancher{
|
||||
Domain: "rancher.localhost",
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
service rancherData
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.port": "1337",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "1337",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
actual := provider.getPort(e.service)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRancherGetDomain(t *testing.T) {
|
||||
provider := &Rancher{
|
||||
Domain: "rancher.localhost",
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
service rancherData
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
},
|
||||
expected: "rancher.localhost",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.domain": "foo.bar",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "foo.bar",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
actual := provider.getDomain(e.service)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRancherGetProtocol(t *testing.T) {
|
||||
provider := &Rancher{
|
||||
Domain: "rancher.localhost",
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
service rancherData
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
},
|
||||
expected: "http",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.protocol": "https",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "https",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
actual := provider.getProtocol(e.service)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRancherGetPassHostHeader(t *testing.T) {
|
||||
provider := &Rancher{
|
||||
Domain: "rancher.localhost",
|
||||
}
|
||||
|
||||
services := []struct {
|
||||
service rancherData
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
},
|
||||
expected: "true",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"traefik.frontend.passHostHeader": "false",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "false",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
actual := provider.getPassHostHeader(e.service)
|
||||
if actual != e.expected {
|
||||
t.Fatalf("expected %q, got %q", e.expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRancherGetLabel(t *testing.T) {
|
||||
services := []struct {
|
||||
service rancherData
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
},
|
||||
expected: "Label not found",
|
||||
},
|
||||
{
|
||||
service: rancherData{
|
||||
Name: "test-service",
|
||||
Labels: map[string]string{
|
||||
"foo": "bar",
|
||||
},
|
||||
},
|
||||
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, e := range services {
|
||||
label, err := getServiceLabel(e.service, "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 TestRancherLoadRancherConfig(t *testing.T) {
|
||||
cases := []struct {
|
||||
services []rancherData
|
||||
expectedFrontends map[string]*types.Frontend
|
||||
expectedBackends map[string]*types.Backend
|
||||
}{
|
||||
{
|
||||
services: []rancherData{},
|
||||
expectedFrontends: map[string]*types.Frontend{},
|
||||
expectedBackends: map[string]*types.Backend{},
|
||||
},
|
||||
{
|
||||
services: []rancherData{
|
||||
{
|
||||
Name: "test/service",
|
||||
Labels: map[string]string{
|
||||
"traefik.port": "80",
|
||||
},
|
||||
Health: "healthy",
|
||||
Containers: []string{"127.0.0.1"},
|
||||
},
|
||||
},
|
||||
expectedFrontends: map[string]*types.Frontend{
|
||||
"frontend-Host-test-service-rancher-localhost": {
|
||||
Backend: "backend-test-service",
|
||||
PassHostHeader: true,
|
||||
EntryPoints: []string{},
|
||||
Priority: 0,
|
||||
|
||||
Routes: map[string]types.Route{
|
||||
"route-frontend-Host-test-service-rancher-localhost": {
|
||||
Rule: "Host:test.service.rancher.localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedBackends: map[string]*types.Backend{
|
||||
"backend-test-service": {
|
||||
Servers: map[string]types.Server{
|
||||
"server-0": {
|
||||
URL: "http://127.0.0.1:80",
|
||||
Weight: 0,
|
||||
},
|
||||
},
|
||||
CircuitBreaker: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
provider := &Rancher{
|
||||
Domain: "rancher.localhost",
|
||||
ExposedByDefault: true,
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
var rancherDataList []rancherData
|
||||
for _, service := range c.services {
|
||||
rancherDataList = append(rancherDataList, service)
|
||||
}
|
||||
|
||||
actualConfig := provider.loadRancherConfig(rancherDataList)
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@ package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/containous/traefik/safe"
|
||||
"github.com/containous/traefik/types"
|
||||
"github.com/docker/libkv/store"
|
||||
|
@@ -1 +1,3 @@
|
||||
mkdocs>=0.9.0
|
||||
mkdocs>=0.16.1,<0.17.0
|
||||
pymdown-extensions==1.4
|
||||
mkdocs-bootswatch==0.4.0
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user