Compare commits

...

59 Commits

Author SHA1 Message Date
Vincent Demeester
331cd173ce Merge pull request #220 from containous/transfer-repo-to-containous-org
Transfer emilevauge/traefik to containous/traefik
2016-02-24 22:22:57 +01:00
Emile Vauge
1881d5eeed Transfer emilevauge/traefik to containous/traefik
Signed-off-by: Emile Vauge <emile@vauge.com>
2016-02-24 17:38:36 +01:00
Vincent Demeester
e0872b6157 Merge pull request #219 from emilevauge/add-traefik-library-image
Add publish to traefik-library-image
2016-02-24 16:11:15 +01:00
Emile Vauge
63fb9c7135 publish binary to traefik-library-image repo
Signed-off-by: Emile Vauge <emile@vauge.com>
2016-02-24 15:48:03 +01:00
Vincent Demeester
9964654495 Merge pull request #183 from keis/consul-catalog
WIP consul catalog provider
2016-02-24 09:35:26 +01:00
David Keijser
ae275c9e60 Consul catalog provider
Fixes #176
2016-02-24 09:23:27 +01:00
David Keijser
4277fe2fdb Bump libcompose 2016-02-24 09:23:27 +01:00
Vincent Demeester
7acc2beae0 Merge pull request #214 from octoblu/add-frontend-determinism
Deterministic frontend registration
2016-02-24 00:05:03 +01:00
Roy van de Water
847deeac79 Deterministic frontend registration
Conflicts:
	server.go
2016-02-22 13:37:54 -07:00
Emile Vauge
ac56c1310c Merge pull request #206 from emilevauge/add-partners
Add partners
2016-02-22 17:33:28 +01:00
emile
7460b343fe Cleanup configuration management 2016-02-22 17:15:45 +01:00
emile
ec16011e31 Add partners, move contributing 2016-02-22 16:26:20 +01:00
emile
71b0e27517 bash instead? 2016-02-22 16:03:57 +01:00
emile
60e9282f0a fixes typo ghr 2016-02-22 15:35:51 +01:00
emile
6cd35a50ce after succes make deploy 2016-02-22 15:17:14 +01:00
emile
b35ad76ec6 rewrite deploy tests 2016-02-22 15:01:00 +01:00
emile
54208f6fc3 travis after_success instead of deploy
Signed-off-by: emile <emile@vauge.com>
2016-02-22 14:33:46 +01:00
emile
6282bf33a0 travis skip_cleanup
Signed-off-by: emile <emile@vauge.com>
2016-02-22 14:11:17 +01:00
Emile Vauge
a1c1958235 Merge pull request #208 from emilevauge/migrate-on-travisci
Migrate on travisci
2016-02-22 13:35:00 +01:00
emile
91b699fbe0 Migrate CI to travis
- Add travis build file
- Use golang alpine image
- Clean scripts a little bit
- Disable CGO for test-integration >_<
2016-02-22 12:20:56 +01:00
Vincent Demeester
3a08655b06 Merge pull request #188 from emilevauge/add-marathon-tls-client-config
Add Marathon TLS client config
2016-02-18 14:23:38 +01:00
emile
9a9c8e5709 Add Marathon TLS client config
Signed-off-by: emile <emile@vauge.com>
2016-02-18 12:42:59 +00:00
Vincent Demeester
c7d34b54aa Merge pull request #205 from emilevauge/bump-go-1.6
Bump go 1.6
2016-02-18 11:07:18 +01:00
emile
8d860c84c8 Add HTTP2 support
Signed-off-by: emile <emile@vauge.com>
2016-02-18 10:31:56 +01:00
emile
1dc086730e Bump golang 1.6 2016-02-18 10:30:41 +01:00
Emile Vauge
5d79e56d30 Merge pull request #193 from dontrebootme/fix192
fix mantl url typo
2016-02-17 23:05:26 +01:00
Vincent Demeester
6e7677de79 Merge pull request #203 from goguardian/alias
Implement `/traefik/alias` for KV stores.
2016-02-17 09:56:43 +01:00
Patrick O'Connor
fab6b8be3c fix mantl url typo 2016-02-16 12:30:33 -08:00
Advait Shinde
f1c1eed437 Fix typo in documentation 2016-02-16 11:55:42 -05:00
Advait Shinde
348ab794c9 Add documentation for /traefik/alias. 2016-02-15 18:46:03 -05:00
Advait Shinde
aacedcc4b3 Implement /traefik/alias for KV stores. 2016-02-15 18:14:57 -05:00
Emile Vauge
786acc961a Merge pull request #200 from vdemeester/some-fixes
Do some build / test-integration fixes
2016-02-13 16:33:42 +01:00
Vincent Demeester
7adffdbd78 Run npm in quiet mode O:)
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2016-02-13 16:14:26 +01:00
Vincent Demeester
e3b519cdd8 Do some build / test-integration fixes
- target generate-webui depends on build-webui
- generate-webui will run only if the folder static does not exists
- create compose project before starting it >_<'', otherwise it does
  nothing :'D

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2016-02-13 16:04:08 +01:00
Vincent Demeester
e9c23195a0 Merge pull request #195 from emilevauge/fix-regression-marathon-backends
Fix regression on marathon backend
2016-02-12 15:31:59 +01:00
emile
c6c3af8099 Fix regression on marathon backend
Signed-off-by: emile <emile@vauge.com>
2016-02-12 15:03:28 +01:00
Emile Vauge
07c077cf94 Merge pull request #189 from vdemeester/fix-make-all
Fix make all
2016-02-10 14:10:26 +01:00
Vincent Demeester
4ac18f1989 Fix make all
Add build-ui as dependent target for all. Otherwise it does not build >_<

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2016-02-10 13:55:47 +01:00
Vincent Demeester
4ecb919787 Merge pull request #178 from emilevauge/add-multiple-entrypoints-support
Add multiple entry points support
2016-02-10 13:54:30 +01:00
emile
4152bd5e26 Update doc with entrypoints 2016-02-10 12:18:50 +01:00
emile
a8cc26fd91 Add entrypoints to providers 2016-02-10 12:14:16 +01:00
emile
81cb00573f Fix tests to accept entrypoints 2016-02-10 12:14:16 +01:00
emile
c22598c8ff Add multiple entry points support, add entry point redirection 2016-02-10 12:14:16 +01:00
Vincent Demeester
bb3b9f61cd Merge pull request #187 from emilevauge/add-max-idle-conns-per-host
Add MaxIdleConnsPerHost. Fixes too many open files error.
2016-02-09 22:49:03 +01:00
emile
49cd7f799e Add MaxIdleConnsPerHost. Fixes too many open files error. 2016-02-09 22:29:01 +01:00
Vincent Demeester
8b334551d8 Merge pull request #185 from ReadmeCritic/master
Update README URLs based on HTTP redirects
2016-02-08 19:15:41 +01:00
ReadmeCritic
5ef6d53d00 Update README URLs based on HTTP redirects 2016-02-08 07:43:43 -08:00
Vincent Demeester
901c9b29bc Merge pull request #171 from tayzlor/fix-170-marathon-getbackend
Fix #170 - getBackend for marathon plugin
2016-02-06 14:06:42 +01:00
Graham Taylor
2d79c500df Fix #170 - lookup backend for marathon plugin so we can specify traefik.backend via labels on the container 2016-02-05 18:01:47 +00:00
Vincent Demeester
d3598021b7 Merge pull request #184 from dontrebootme/master
Updated getEscapedName with slash to dash instead of space
2016-02-03 22:09:52 +01:00
Patrick O'Connor
31e0340959 Updated getEscapedName with slash to dash instead of space
Remove only the first slash, convert the rest to dash
2016-02-03 12:07:08 -08:00
Emile Vauge
cb46e8751b Merge pull request #177 from Hugues-Antoine/master
Updating documentation to make a more explicit usage of the traefik.frontend.rule rule when combined with traefik.frontend.value
2016-01-29 19:46:12 +01:00
Hugues-Antoine
739a836c52 Updating documentation to make a more explicit usage of the traefik.frontend.rule rule when combined with traefik.frontend.value 2016-01-29 18:59:31 +01:00
Vincent Demeester
1cbe00d613 Merge pull request #161 from emilevauge/add-env-args
Add environnement variables and arguments configuration
2016-01-29 11:49:33 +01:00
Vincent Demeester
10d92ca176 Merge branch 'master' into add-env-args 2016-01-29 11:34:15 +01:00
emile
f1b62b45f4 Fixes following review 2016-01-27 19:47:20 +01:00
emile
8adadaa5d4 Add launch configuration documentation 2016-01-27 13:56:46 +01:00
emile
35070f7c1c Use of Viper and cobra 2016-01-27 13:56:46 +01:00
emile
a0b15a0efd Main file refactoring, extract a Server object 2016-01-27 13:55:40 +01:00
69 changed files with 3227 additions and 786 deletions

78
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,78 @@
# Contributing
### Building
You need either [Docker](https://github.com/docker/docker) and `make`, or `go` and `glide` in order to build traefik.
#### Setting up your `go` environment
- You need `go` v1.5
- You need to set `export GO15VENDOREXPERIMENT=1` environment variable
- You need `go-bindata` to be able to use `go generate` command (needed to build) : `go get github.com/jteeuwen/go-bindata/...`.
- If you clone Træfɪk into something like `~/go/src/github.com/traefik`, your `GOPATH` variable will have to be set to `~/go`: export `GOPATH=~/go`.
#### Using `Docker` and `Makefile`
You need to run the `binary` target. This will create binaries for Linux platform in the `dist` folder.
```bash
$ make binary
docker build -t "traefik-dev:no-more-godep-ever" -f build.Dockerfile .
Sending build context to Docker daemon 295.3 MB
Step 0 : FROM golang:1.5
---> 8c6473912976
Step 1 : RUN go get github.com/Masterminds/glide
[...]
docker run --rm -v "/var/run/docker.sock:/var/run/docker.sock" -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -v "/home/emile/dev/go/src/github.com/containous/traefik/"dist":/go/src/github.com/containous/traefik/"dist"" "traefik-dev:no-more-godep-ever" ./script/make.sh generate binary
---> Making bundle: generate (in .)
removed 'gen.go'
---> Making bundle: binary (in .)
$ ls dist/
traefik*
```
#### Using `glide`
The idea behind `glide` is the following :
- when checkout(ing) a project, **run `glide up --quick`** to install
(`go get …`) the dependencies in the `GOPATH`.
- if you need another dependency, import and use it in
the source, and **run `glide get github.com/Masterminds/cookoo`** to save it in
`vendor` and add it to your `glide.yaml`.
```bash
$ glide up --quick
# generate
$ go generate
# Simple go build
$ go build
# Using gox to build multiple platform
$ gox "linux darwin" "386 amd64 arm" \
-output="dist/traefik_{{.OS}}-{{.Arch}}"
# run other commands like tests
$ go test ./...
ok _/home/vincent/src/github/vdemeester/traefik 0.004s
```
### Tests
You can run unit tests using the `test-unit` target and the
integration test using the `test-integration` target.
```bash
$ make test-unit
docker build -t "traefik-dev:your-feature-branch" -f build.Dockerfile .
# […]
docker run --rm -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -v "/home/vincent/src/github/vdemeester/traefik/dist:/go/src/github.com/containous/traefik/dist" "traefik-dev:your-feature-branch" ./script/make.sh generate test-unit
---> Making bundle: generate (in .)
removed 'gen.go'
---> Making bundle: test-unit (in .)
+ go test -cover -coverprofile=cover.out .
ok github.com/containous/traefik 0.005s coverage: 4.1% of statements
Test success
```

27
.travis.yml Normal file
View File

@@ -0,0 +1,27 @@
env:
REPO: $TRAVIS_REPO_SLUG
VERSION: v1.0.alpha.$TRAVIS_COMMIT
sudo: required
services:
- docker
install:
- sudo service docker stop
- sudo curl https://get.docker.com/builds/Linux/x86_64/docker-1.10.1 -o /usr/bin/docker
- sudo chmod +x /usr/bin/docker
- sudo service docker start
before_script:
- make validate
- make binary
script:
- make test-unit
- make test-integration
- make crossbinary
- make image
after_success:
- make deploy

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015 Emile Vauge, emile@vauge.com
Copyright (c) 2016 Containous SAS, Emile Vauge, emile@vauge.com
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
THE SOFTWARE.

View File

@@ -4,30 +4,32 @@ TRAEFIK_ENVS := \
-e OS_ARCH_ARG \
-e OS_PLATFORM_ARG \
-e TESTFLAGS \
-e CIRCLECI
-e VERSION
SRCS = $(shell git ls-files '*.go' | grep -v '^external/')
BIND_DIR := "dist"
TRAEFIK_MOUNT := -v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/emilevauge/traefik/$(BIND_DIR)"
TRAEFIK_MOUNT := -v "$(CURDIR)/$(BIND_DIR):/go/src/github.com/containous/traefik/$(BIND_DIR)"
GIT_BRANCH := $(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)
TRAEFIK_DEV_IMAGE := traefik-dev$(if $(GIT_BRANCH),:$(GIT_BRANCH))
REPONAME := $(shell echo $(REPO) | tr '[:upper:]' '[:lower:]')
TRAEFIK_IMAGE := $(if $(REPONAME),$(REPONAME),"emilevauge/traefik")
TRAEFIK_IMAGE := $(if $(REPONAME),$(REPONAME),"containous/traefik")
INTEGRATION_OPTS := $(if $(MAKE_DOCKER_HOST),-e "DOCKER_HOST=$(MAKE_DOCKER_HOST)", -v "/var/run/docker.sock:/var/run/docker.sock")
DOCKER_RUN_TRAEFIK := docker run $(if $(CIRCLECI),,--rm) $(INTEGRATION_OPTS) -it $(TRAEFIK_ENVS) $(TRAEFIK_MOUNT) "$(TRAEFIK_DEV_IMAGE)"
DOCKER_RUN_TRAEFIK := docker run $(INTEGRATION_OPTS) -it $(TRAEFIK_ENVS) $(TRAEFIK_MOUNT) "$(TRAEFIK_DEV_IMAGE)"
print-%: ; @echo $*=$($*)
default: binary
all: build
all: generate-webui build
$(DOCKER_RUN_TRAEFIK) ./script/make.sh
binary: build-webui generate-webui build
binary: generate-webui build
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate binary
crossbinary: build-webui generate-webui build
crossbinary: generate-webui build
$(DOCKER_RUN_TRAEFIK) ./script/make.sh generate crossbinary
test: build
@@ -74,7 +76,18 @@ run-dev:
go build
./traefik
generate-webui:
mkdir -p static
docker run --rm -v "$$PWD/static":'/src/static' traefik-webui gulp
echo 'For more informations show `webui/readme.md`' > $$PWD/static/DONT-EDIT-FILES-IN-THIS-DIRECTORY.md
generate-webui: build-webui
if [ ! -d "static" ]; then \
mkdir -p static; \
docker run --rm -v "$$PWD/static":'/src/static' traefik-webui gulp; \
echo 'For more informations show `webui/readme.md`' > $$PWD/static/DONT-EDIT-FILES-IN-THIS-DIRECTORY.md; \
fi
lint:
$(foreach file,$(SRCS),golint $(file) || exit;)
fmt:
gofmt -s -l -w $(SRCS)
deploy:
./script/deploy.sh

105
README.md
View File

@@ -1,13 +1,15 @@
![Træfɪk](http://traefik.github.io/traefik.logo.svg "Træfɪk")
___
[![Circle CI](https://circleci.com/gh/emilevauge/traefik.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/emilevauge/traefik)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/EmileVauge/traefik/blob/master/LICENSE.md)
[![Build Status](https://travis-ci.org/containous/traefik.svg?branch=master)](https://travis-ci.org/containous/traefik)
[![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/containous/traefik/blob/master/LICENSE.md)
[![Join the chat at https://traefik.herokuapp.com](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://traefik.herokuapp.com)
[![Twitter](https://img.shields.io/twitter/follow/traefikproxy.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefikproxy)
Træfɪk is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
It supports several backends ([Docker :whale:](https://www.docker.com/), [Mesos/Marathon](https://mesosphere.github.io/marathon/), [Consul](https://consul.io/), [Etcd](https://coreos.com/etcd/), [Zookeeper](https://zookeeper.apache.org), [BoltDB](https://github.com/boltdb/bolt), Rest API, file...) to manage its configuration automatically and dynamically.
It supports several backends ([Docker :whale:](https://www.docker.com/), [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.
## Features
@@ -22,11 +24,12 @@ It supports several backends ([Docker :whale:](https://www.docker.com/), [Mesos/
- Circuit breakers on backends
- Round Robin, rebalancer load-balancers
- Rest Metrics
- Tiny docker image included
- Tiny docker image included [![Image Layers](https://badge.imagelayers.io/containous/traefik:latest.svg)](https://imagelayers.io/?images=containous/traefik:latest)
- SSL backends support
- SSL frontend support
- Clean AngularJS Web UI
- Websocket support
- HTTP/2 support
## Demo
@@ -43,114 +46,64 @@ You can access to a simple HTML frontend of Træfik.
## Plumbing
- [Oxy](https://github.com/mailgun/oxy/): an awsome proxy library made by Mailgun guys
- [Oxy](https://github.com/vulcand/oxy): an awsome proxy library made by Mailgun guys
- [Gorilla mux](https://github.com/gorilla/mux): famous request router
- [Negroni](https://github.com/codegangsta/negroni): web middlewares made simple
- [Manners](https://github.com/mailgun/manners): graceful shutdown of http.Handler servers
## Quick start
- The simple way: grab the latest binary from the [releases](https://github.com/emilevauge/traefik/releases) page and just run it with the [sample configuration file](https://raw.githubusercontent.com/EmileVauge/traefik/master/traefik.sample.toml):
- The simple way: grab the latest binary from the [releases](https://github.com/containous/traefik/releases) page and just run it with the [sample configuration file](https://raw.githubusercontent.com/containous/traefik/master/traefik.sample.toml):
```shell
./traefik traefik.toml
./traefik -c traefik.toml
```
- Use the tiny Docker image:
```shell
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/traefik.toml emilevauge/traefik
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/etc/traefik/traefik.toml containous/traefik
```
- From sources:
```shell
git clone https://github.com/EmileVauge/traefik
git clone https://github.com/containous/traefik
```
## Documentation
You can find the complete documentation [here](docs/index.md).
## Benchmarks
Refer to the [benchmarks section](docs/index.md#benchmarks) in the documentation.
## Contributing
### Building
Please refer to [this section](.github/CONTRIBUTING.md).
You need either [Docker](https://github.com/docker/docker) and `make`, or `go` and `glide` in order to build traefik.
## Træfɪk here and there
#### Setting up your `go` environment
These projects use Træfɪk internally. If your company uses Træfɪk, we would be glad to get your feedback :) Contact us on [![Join the chat at https://traefik.herokuapp.com](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://traefik.herokuapp.com)
- You need `go` v1.5
- You need to set `export GO15VENDOREXPERIMENT=1` environment variable
- You need `go-bindata` to be able to use `go generate` command (needed to build) : `go get github.com/jteeuwen/go-bindata/...`.
- If you clone Træfɪk into something like `~/go/src/github.com/traefik`, your `GOPATH` variable will have to be set to `~/go`: export `GOPATH=~/go`.
- Project [Mantl](https://mantl.io/) from Cisco
#### Using `Docker` and `Makefile`
![Web UI Providers](docs/img/mantl-logo.png)
> Mantl is a modern platform for rapidly deploying globally distributed services. A container orchestrator, docker, a network stack, something to pool your logs, something to monitor health, a sprinkle of service discovery and some automation.
You need to run the `binary` target. This will create binaries for Linux platform in the `dist` folder.
- Project [Apollo](http://capgemini.github.io/devops/apollo/) from Cap Gemini
```bash
$ make binary
docker build -t "traefik-dev:no-more-godep-ever" -f build.Dockerfile .
Sending build context to Docker daemon 295.3 MB
Step 0 : FROM golang:1.5
---> 8c6473912976
Step 1 : RUN go get github.com/Masterminds/glide
[...]
docker run --rm -v "/var/run/docker.sock:/var/run/docker.sock" -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -v "/home/emile/dev/go/src/github.com/emilevauge/traefik/"dist":/go/src/github.com/emilevauge/traefik/"dist"" "traefik-dev:no-more-godep-ever" ./script/make.sh generate binary
---> Making bundle: generate (in .)
removed 'gen.go'
![Web UI Providers](docs/img/apollo-logo.png)
> Apollo is an open source project to aid with building and deploying IAAS and PAAS services. It is particularly geared towards managing containerized applications across multiple hosts, and big data type workloads. Apollo leverages other open source components to provide basic mechanisms for deployment, maintenance, and scaling of infrastructure and applications.
---> Making bundle: binary (in .)
## Partners
$ ls dist/
traefik*
```
[![Zenika](docs/img/zenika.logo.png)](https://zenika.com)
#### Using `glide`
Zenika is one of the leading providers of professional Open Source services and agile methodologies in
Europe. We provide consulting, development, training and support for the worlds leading Open Source
software products.
The idea behind `glide` is the following :
- when checkout(ing) a project, **run `glide up --quick`** to install
(`go get …`) the dependencies in the `GOPATH`.
- if you need another dependency, import and use it in
the source, and **run `glide get github.com/Masterminds/cookoo`** to save it in
`vendor` and add it to your `glide.yaml`.
```bash
$ glide up --quick
# generate
$ go generate
# Simple go build
$ go build
# Using gox to build multiple platform
$ gox "linux darwin" "386 amd64 arm" \
-output="dist/traefik_{{.OS}}-{{.Arch}}"
# run other commands like tests
$ go test ./...
ok _/home/vincent/src/github/vdemeester/traefik 0.004s
```
[![Asteris](docs/img/asteris.logo.png)](https://aster.is)
### Tests
You can run unit tests using the `test-unit` target and the
integration test using the `test-integration` target.
```bash
$ make test-unit
docker build -t "traefik-dev:your-feature-branch" -f build.Dockerfile .
# […]
docker run --rm -it -e OS_ARCH_ARG -e OS_PLATFORM_ARG -e TESTFLAGS -v "/home/vincent/src/github/vdemeester/traefik/dist:/go/src/github.com/emilevauge/traefik/dist" "traefik-dev:your-feature-branch" ./script/make.sh generate test-unit
---> Making bundle: generate (in .)
removed 'gen.go'
---> Making bundle: test-unit (in .)
+ go test -cover -coverprofile=cover.out .
ok github.com/emilevauge/traefik 0.005s coverage: 4.1% of statements
Test success
```
Founded in 2014, Asteris creates next-generation infrastructure software for the modern datacenter. Asteris writes software that makes it easy for companies to implement continuous delivery and realtime data pipelines. We support the HashiCorp stack, along with Kubernetes, Apache Mesos, Spark and Kafka. We're core committers on mantl.io, consul-cli and mesos-consul.
.

View File

@@ -7,7 +7,6 @@ import (
"net/http"
log "github.com/Sirupsen/logrus"
"github.com/gorilla/mux"
)
// OxyLogger implements oxy Logger interface with logrus.
@@ -33,10 +32,3 @@ func notFoundHandler(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
//templatesRenderer.HTML(w, http.StatusNotFound, "notFound", nil)
}
// LoadDefaultConfig returns a default gorrilla.mux router from the specified configuration.
func LoadDefaultConfig(globalConfiguration *GlobalConfiguration) *mux.Router {
router := mux.NewRouter()
router.NotFoundHandler = http.HandlerFunc(notFoundHandler)
return router
}

View File

@@ -1,13 +1,14 @@
FROM golang:1.5.3
FROM golang:1.6.0-alpine
RUN apk update && apk add git bash gcc
RUN go get github.com/Masterminds/glide
RUN go get github.com/mitchellh/gox
RUN go get github.com/tcnksm/ghr
RUN go get github.com/jteeuwen/go-bindata/...
RUN go get github.com/golang/lint/golint
# Which docker version to test on
ENV DOCKER_VERSION 1.6.2
ENV DOCKER_VERSION 1.10.1
# enable GO15VENDOREXPERIMENT
ENV GO15VENDOREXPERIMENT 1
@@ -20,9 +21,9 @@ RUN set -ex; \
# Set the default Docker to be run
RUN ln -s /usr/local/bin/docker-${DOCKER_VERSION} /usr/local/bin/docker
WORKDIR /go/src/github.com/emilevauge/traefik
WORKDIR /go/src/github.com/containous/traefik
COPY glide.yaml glide.yaml
RUN glide up --quick
COPY . /go/src/github.com/emilevauge/traefik
COPY . /go/src/github.com/containous/traefik

193
cmd.go Normal file
View File

@@ -0,0 +1,193 @@
/*
Copyright
*/
package main
import (
"encoding/json"
fmtlog "log"
"os"
"strings"
"time"
log "github.com/Sirupsen/logrus"
"github.com/containous/traefik/middlewares"
"github.com/containous/traefik/provider"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"net/http"
)
var traefikCmd = &cobra.Command{
Use: "traefik",
Short: "traefik, a modern reverse proxy",
Long: `traefik is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
Complete documentation is available at http://traefik.io`,
Run: func(cmd *cobra.Command, args []string) {
run()
},
}
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print version",
Long: `Print version`,
Run: func(cmd *cobra.Command, args []string) {
fmtlog.Println(Version + " built on the " + BuildDate)
os.Exit(0)
},
}
var arguments = struct {
GlobalConfiguration
web bool
file bool
docker bool
dockerTLS bool
marathon bool
consul bool
consulCatalog bool
zookeeper bool
etcd bool
boltdb bool
}{
GlobalConfiguration{
EntryPoints: make(EntryPoints),
Docker: &provider.Docker{
TLS: &provider.DockerTLS{},
},
File: &provider.File{},
Web: &WebProvider{},
Marathon: &provider.Marathon{},
Consul: &provider.Consul{},
ConsulCatalog: &provider.ConsulCatalog{},
Zookeeper: &provider.Zookepper{},
Etcd: &provider.Etcd{},
Boltdb: &provider.BoltDb{},
},
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
}
func init() {
traefikCmd.AddCommand(versionCmd)
traefikCmd.PersistentFlags().StringP("configFile", "c", "", "Configuration file to use (TOML, JSON, YAML, HCL).")
traefikCmd.PersistentFlags().StringP("graceTimeOut", "g", "10", "Timeout in seconds. Duration to give active requests a chance to finish during hot-reloads")
traefikCmd.PersistentFlags().String("accessLogsFile", "log/access.log", "Access logs file")
traefikCmd.PersistentFlags().String("traefikLogsFile", "log/traefik.log", "Traefik logs file")
traefikCmd.PersistentFlags().Var(&arguments.EntryPoints, "entryPoints", "Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'")
traefikCmd.PersistentFlags().Var(&arguments.DefaultEntryPoints, "defaultEntryPoints", "Entrypoints to be used by frontends that do not specify any entrypoint")
traefikCmd.PersistentFlags().StringP("logLevel", "l", "ERROR", "Log level")
traefikCmd.PersistentFlags().DurationVar(&arguments.ProvidersThrottleDuration, "providersThrottleDuration", time.Duration(2*time.Second), "Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time.")
traefikCmd.PersistentFlags().Int("maxIdleConnsPerHost", 0, "If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used")
traefikCmd.PersistentFlags().BoolVar(&arguments.web, "web", false, "Enable Web backend")
traefikCmd.PersistentFlags().StringVar(&arguments.Web.Address, "web.address", ":8080", "Web administration port")
traefikCmd.PersistentFlags().StringVar(&arguments.Web.CertFile, "web.cerFile", "", "SSL certificate")
traefikCmd.PersistentFlags().StringVar(&arguments.Web.KeyFile, "web.keyFile", "", "SSL certificate")
traefikCmd.PersistentFlags().BoolVar(&arguments.Web.ReadOnly, "web.readOnly", false, "Enable read only API")
traefikCmd.PersistentFlags().BoolVar(&arguments.file, "file", false, "Enable File backend")
traefikCmd.PersistentFlags().BoolVar(&arguments.File.Watch, "file.watch", true, "Watch provider")
traefikCmd.PersistentFlags().StringVar(&arguments.File.Filename, "file.filename", "", "Override default configuration template. For advanced users :)")
traefikCmd.PersistentFlags().BoolVar(&arguments.docker, "docker", false, "Enable Docker backend")
traefikCmd.PersistentFlags().BoolVar(&arguments.Docker.Watch, "docker.watch", true, "Watch provider")
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.Filename, "docker.filename", "", "Override default configuration template. For advanced users :)")
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.Endpoint, "docker.endpoint", "unix:///var/run/docker.sock", "Docker server endpoint. Can be a tcp or a unix socket endpoint")
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.Domain, "docker.domain", "", "Default domain used")
traefikCmd.PersistentFlags().BoolVar(&arguments.dockerTLS, "docker.tls", false, "Enable Docker TLS support")
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.TLS.CA, "docker.tls.ca", "", "TLS CA")
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.TLS.Cert, "docker.tls.cert", "", "TLS cert")
traefikCmd.PersistentFlags().StringVar(&arguments.Docker.TLS.Key, "docker.tls.key", "", "TLS key")
traefikCmd.PersistentFlags().BoolVar(&arguments.Docker.TLS.InsecureSkipVerify, "docker.tls.insecureSkipVerify", false, "TLS insecure skip verify")
traefikCmd.PersistentFlags().BoolVar(&arguments.marathon, "marathon", false, "Enable Marathon backend")
traefikCmd.PersistentFlags().BoolVar(&arguments.Marathon.Watch, "marathon.watch", true, "Watch provider")
traefikCmd.PersistentFlags().StringVar(&arguments.Marathon.Filename, "marathon.filename", "", "Override default configuration template. For advanced users :)")
traefikCmd.PersistentFlags().StringVar(&arguments.Marathon.Endpoint, "marathon.endpoint", "http://127.0.0.1:8080", "Marathon server endpoint. You can also specify multiple endpoint for Marathon")
traefikCmd.PersistentFlags().StringVar(&arguments.Marathon.Domain, "marathon.domain", "", "Default domain used")
traefikCmd.PersistentFlags().StringVar(&arguments.Marathon.NetworkInterface, "marathon.networkInterface", "eth0", "Network interface used to call Marathon web services. Needed in case of multiple network interfaces")
traefikCmd.PersistentFlags().BoolVar(&arguments.consul, "consul", false, "Enable Consul backend")
traefikCmd.PersistentFlags().BoolVar(&arguments.Consul.Watch, "consul.watch", true, "Watch provider")
traefikCmd.PersistentFlags().StringVar(&arguments.Consul.Filename, "consul.filename", "", "Override default configuration template. For advanced users :)")
traefikCmd.PersistentFlags().StringVar(&arguments.Consul.Endpoint, "consul.endpoint", "127.0.0.1:8500", "Consul server endpoint")
traefikCmd.PersistentFlags().StringVar(&arguments.Consul.Prefix, "consul.prefix", "/traefik", "Prefix used for KV store")
traefikCmd.PersistentFlags().BoolVar(&arguments.consulCatalog, "consulCatalog", false, "Enable Consul catalog backend")
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Domain, "consulCatalog.domain", "", "Default domain used")
traefikCmd.PersistentFlags().StringVar(&arguments.ConsulCatalog.Endpoint, "consulCatalog.endpoint", "127.0.0.1:8500", "Consul server endpoint")
traefikCmd.PersistentFlags().BoolVar(&arguments.zookeeper, "zookeeper", false, "Enable Zookeeper backend")
traefikCmd.PersistentFlags().BoolVar(&arguments.Zookeeper.Watch, "zookeeper.watch", true, "Watch provider")
traefikCmd.PersistentFlags().StringVar(&arguments.Zookeeper.Filename, "zookeeper.filename", "", "Override default configuration template. For advanced users :)")
traefikCmd.PersistentFlags().StringVar(&arguments.Zookeeper.Endpoint, "zookeeper.endpoint", "127.0.0.1:2181", "Zookeeper server endpoint")
traefikCmd.PersistentFlags().StringVar(&arguments.Zookeeper.Prefix, "zookeeper.prefix", "/traefik", "Prefix used for KV store")
traefikCmd.PersistentFlags().BoolVar(&arguments.etcd, "etcd", false, "Enable Etcd backend")
traefikCmd.PersistentFlags().BoolVar(&arguments.Etcd.Watch, "etcd.watch", true, "Watch provider")
traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.Filename, "etcd.filename", "", "Override default configuration template. For advanced users :)")
traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.Endpoint, "etcd.endpoint", "127.0.0.1:4001", "Etcd server endpoint")
traefikCmd.PersistentFlags().StringVar(&arguments.Etcd.Prefix, "etcd.prefix", "/traefik", "Prefix used for KV store")
traefikCmd.PersistentFlags().BoolVar(&arguments.boltdb, "boltdb", false, "Enable Boltdb backend")
traefikCmd.PersistentFlags().BoolVar(&arguments.Boltdb.Watch, "boltdb.watch", true, "Watch provider")
traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Filename, "boltdb.filename", "", "Override default configuration template. For advanced users :)")
traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Endpoint, "boltdb.endpoint", "127.0.0.1:4001", "Boltdb server endpoint")
traefikCmd.PersistentFlags().StringVar(&arguments.Boltdb.Prefix, "boltdb.prefix", "/traefik", "Prefix used for KV store")
viper.BindPFlag("configFile", traefikCmd.PersistentFlags().Lookup("configFile"))
viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut"))
//viper.BindPFlag("defaultEntryPoints", traefikCmd.PersistentFlags().Lookup("defaultEntryPoints"))
viper.BindPFlag("logLevel", traefikCmd.PersistentFlags().Lookup("logLevel"))
// TODO: wait for this issue to be corrected: https://github.com/spf13/viper/issues/105
viper.BindPFlag("providersThrottleDuration", traefikCmd.PersistentFlags().Lookup("providersThrottleDuration"))
viper.BindPFlag("maxIdleConnsPerHost", traefikCmd.PersistentFlags().Lookup("maxIdleConnsPerHost"))
viper.SetDefault("providersThrottleDuration", time.Duration(2*time.Second))
viper.SetDefault("logLevel", "ERROR")
}
func run() {
fmtlog.SetFlags(fmtlog.Lshortfile | fmtlog.LstdFlags)
// load global configuration
globalConfiguration := LoadConfiguration()
http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = globalConfiguration.MaxIdleConnsPerHost
loggerMiddleware := middlewares.NewLogger(globalConfiguration.AccessLogsFile)
defer loggerMiddleware.Close()
// logging
level, err := log.ParseLevel(strings.ToLower(globalConfiguration.LogLevel))
if err != nil {
log.Fatal("Error getting level", err)
}
log.SetLevel(level)
if len(globalConfiguration.TraefikLogsFile) > 0 {
fi, err := os.OpenFile(globalConfiguration.TraefikLogsFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
defer fi.Close()
if err != nil {
log.Fatal("Error opening file", err)
} else {
log.SetOutput(fi)
log.SetFormatter(&log.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true})
}
} else {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableSorting: true})
}
jsonConf, _ := json.Marshal(globalConfiguration)
log.Debugf("Global configuration loaded %s", string(jsonConf))
server := NewServer(*globalConfiguration)
server.Start()
defer server.Close()
log.Info("Shutting down")
}

View File

@@ -1,34 +1,177 @@
package main
import (
"errors"
"fmt"
fmtlog "log"
"regexp"
"strings"
"time"
"github.com/BurntSushi/toml"
"github.com/emilevauge/traefik/provider"
"github.com/emilevauge/traefik/types"
"github.com/containous/traefik/provider"
"github.com/containous/traefik/types"
"github.com/mitchellh/mapstructure"
"github.com/spf13/viper"
)
// GlobalConfiguration holds global configuration (with providers, etc.).
// It's populated from the traefik configuration file passed as an argument to the binary.
type GlobalConfiguration struct {
Port string
GraceTimeOut int64
AccessLogsFile string
TraefikLogsFile string
Certificates []Certificate
LogLevel string
EntryPoints EntryPoints
DefaultEntryPoints DefaultEntryPoints
ProvidersThrottleDuration time.Duration
MaxIdleConnsPerHost int
Docker *provider.Docker
File *provider.File
Web *WebProvider
Marathon *provider.Marathon
Consul *provider.Consul
ConsulCatalog *provider.ConsulCatalog
Etcd *provider.Etcd
Zookeeper *provider.Zookepper
Boltdb *provider.BoltDb
}
// DefaultEntryPoints holds default entry points
type DefaultEntryPoints []string
// String is the method to format the flag's value, part of the flag.Value interface.
// The String method's output will be used in diagnostics.
func (dep *DefaultEntryPoints) String() string {
return fmt.Sprintf("%#v", dep)
}
// Set is the method to set the flag value, part of the flag.Value interface.
// Set's argument is a string to be parsed to set the flag.
// It's a comma-separated list, so we split it.
func (dep *DefaultEntryPoints) Set(value string) error {
entrypoints := strings.Split(value, ",")
if len(entrypoints) == 0 {
return errors.New("Bad DefaultEntryPoints format: " + value)
}
for _, entrypoint := range entrypoints {
*dep = append(*dep, entrypoint)
}
return nil
}
// Type is type of the struct
func (dep *DefaultEntryPoints) Type() string {
return fmt.Sprint("defaultentrypoints²")
}
// EntryPoints holds entry points configuration of the reverse proxy (ip, port, TLS...)
type EntryPoints map[string]*EntryPoint
// String is the method to format the flag's value, part of the flag.Value interface.
// The String method's output will be used in diagnostics.
func (ep *EntryPoints) String() string {
return ""
}
// Set is the method to set the flag value, part of the flag.Value interface.
// Set's argument is a string to be parsed to set the flag.
// It's a comma-separated list, so we split it.
func (ep *EntryPoints) Set(value string) error {
regex := regexp.MustCompile("(?:Name:(?P<Name>\\S*))\\s*(?:Address:(?P<Address>\\S*))?\\s*(?:TLS:(?P<TLS>\\S*))?\\s*(?:Redirect.EntryPoint:(?P<RedirectEntryPoint>\\S*))?\\s*(?:Redirect.Regex:(?P<RedirectRegex>\\S*))?\\s*(?:Redirect.Replacement:(?P<RedirectReplacement>\\S*))?")
match := regex.FindAllStringSubmatch(value, -1)
if match == nil {
return errors.New("Bad EntryPoints format: " + value)
}
matchResult := match[0]
result := make(map[string]string)
for i, name := range regex.SubexpNames() {
if i != 0 {
result[name] = matchResult[i]
}
}
var tls *TLS
if len(result["TLS"]) > 0 {
certs := Certificates{}
certs.Set(result["TLS"])
tls = &TLS{
Certificates: certs,
}
}
var redirect *Redirect
if len(result["RedirectEntryPoint"]) > 0 || len(result["RedirectRegex"]) > 0 || len(result["RedirectReplacement"]) > 0 {
redirect = &Redirect{
EntryPoint: result["RedirectEntryPoint"],
Regex: result["RedirectRegex"],
Replacement: result["RedirectReplacement"],
}
}
(*ep)[result["Name"]] = &EntryPoint{
Address: result["Address"],
TLS: tls,
Redirect: redirect,
}
return nil
}
// Type is type of the struct
func (ep *EntryPoints) Type() string {
return fmt.Sprint("entrypoints²")
}
// EntryPoint holds an entry point configuration of the reverse proxy (ip, port, TLS...)
type EntryPoint struct {
Network string
Address string
TLS *TLS
Redirect *Redirect
}
// Redirect configures a redirection of an entry point to another, or to an URL
type Redirect struct {
EntryPoint string
Regex string
Replacement string
}
// TLS configures TLS for an entry point
type TLS struct {
Certificates Certificates
}
// Certificates defines traefik certificates type
type Certificates []Certificate
// String is the method to format the flag's value, part of the flag.Value interface.
// The String method's output will be used in diagnostics.
func (certs *Certificates) String() string {
if len(*certs) == 0 {
return ""
}
return (*certs)[0].CertFile + "," + (*certs)[0].KeyFile
}
// Set is the method to set the flag value, part of the flag.Value interface.
// Set's argument is a string to be parsed to set the flag.
// It's a comma-separated list, so we split it.
func (certs *Certificates) Set(value string) error {
files := strings.Split(value, ",")
if len(files) != 2 {
return errors.New("Bad certificates format: " + value)
}
*certs = append(*certs, Certificate{
CertFile: files[0],
KeyFile: files[1],
})
return nil
}
// Type is type of the struct
func (certs *Certificates) Type() string {
return fmt.Sprint("certificates")
}
// Certificate holds a SSL cert/key pair
type Certificate struct {
CertFile string
@@ -37,23 +180,101 @@ type Certificate struct {
// NewGlobalConfiguration returns a GlobalConfiguration with default values.
func NewGlobalConfiguration() *GlobalConfiguration {
globalConfiguration := new(GlobalConfiguration)
// default values
globalConfiguration.Port = ":80"
globalConfiguration.GraceTimeOut = 10
globalConfiguration.LogLevel = "ERROR"
globalConfiguration.ProvidersThrottleDuration = time.Duration(2 * time.Second)
return globalConfiguration
return new(GlobalConfiguration)
}
// LoadFileConfig returns a GlobalConfiguration from reading the specified file (a toml file).
func LoadFileConfig(file string) *GlobalConfiguration {
// LoadConfiguration returns a GlobalConfiguration.
func LoadConfiguration() *GlobalConfiguration {
configuration := NewGlobalConfiguration()
if _, err := toml.DecodeFile(file, configuration); err != nil {
viper.SetEnvPrefix("traefik")
viper.SetConfigType("toml")
viper.AutomaticEnv()
if len(viper.GetString("configFile")) > 0 {
viper.SetConfigFile(viper.GetString("configFile"))
} else {
viper.SetConfigName("traefik") // name of config file (without extension)
}
viper.AddConfigPath("/etc/traefik/") // path to look for the config file in
viper.AddConfigPath("$HOME/.traefik/") // call multiple times to add many search paths
viper.AddConfigPath(".") // optionally look for config in the working directory
if err := viper.ReadInConfig(); err != nil {
fmtlog.Fatalf("Error reading file: %s", err)
}
if len(arguments.EntryPoints) > 0 {
viper.Set("entryPoints", arguments.EntryPoints)
}
if len(arguments.DefaultEntryPoints) > 0 {
viper.Set("defaultEntryPoints", arguments.DefaultEntryPoints)
}
if arguments.web {
viper.Set("web", arguments.Web)
}
if arguments.file {
viper.Set("file", arguments.File)
}
if !arguments.dockerTLS {
arguments.Docker.TLS = nil
}
if arguments.docker {
viper.Set("docker", arguments.Docker)
}
if arguments.marathon {
viper.Set("marathon", arguments.Marathon)
}
if arguments.consul {
viper.Set("consul", arguments.Consul)
}
if arguments.consulCatalog {
viper.Set("consulCatalog", arguments.ConsulCatalog)
}
if arguments.zookeeper {
viper.Set("zookeeper", arguments.Zookeeper)
}
if arguments.etcd {
viper.Set("etcd", arguments.Etcd)
}
if arguments.boltdb {
viper.Set("boltdb", arguments.Boltdb)
}
if err := unmarshal(&configuration); err != nil {
fmtlog.Fatalf("Error reading file: %s", err)
}
if len(configuration.EntryPoints) == 0 {
configuration.EntryPoints = make(map[string]*EntryPoint)
configuration.EntryPoints["http"] = &EntryPoint{
Address: ":80",
}
configuration.DefaultEntryPoints = []string{"http"}
}
if configuration.File != nil && len(configuration.File.Filename) == 0 {
// no filename, setting to global config file
configuration.File.Filename = viper.ConfigFileUsed()
}
return configuration
}
func unmarshal(rawVal interface{}) error {
config := &mapstructure.DecoderConfig{
DecodeHook: mapstructure.StringToTimeDurationHookFunc(),
Metadata: nil,
Result: rawVal,
WeaklyTypedInput: true,
}
decoder, err := mapstructure.NewDecoder(config)
if err != nil {
return err
}
err = decoder.Decode(viper.AllSettings())
if err != nil {
return err
}
return nil
}
type configs map[string]*types.Configuration

BIN
docs/img/apollo-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
docs/img/asteris.logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
docs/img/mantl-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
docs/img/zenika.logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@@ -5,15 +5,18 @@ ___
# <a id="top"></a> Documentation
- [Basics](#basics)
- [Launch configuration](#launch)
- [Global configuration](#global)
- [File backend](#file)
- [API backend](#api)
- [Docker backend](#docker)
- [Mesos/Marathon backend](#marathon)
- [Consul backend](#consul)
- [Consul catalog backend](#consulcatalog)
- [Etcd backend](#etcd)
- [Zookeeper backend](#zk)
- [Boltdb backend](#boltdb)
- [Atomic configuration changes](#atomicconfig)
- [Benchmarks](#benchmarks)
@@ -59,6 +62,103 @@ For example:
- `LatencyAtQuantileMS(50.0) > 50`
- `ResponseCodeRatio(500, 600, 0, 600) > 0.5`
## <a id="launch"></a> Launch configuration
Træfɪk can be configured using a TOML file configuration, arguments, or both.
By default, Træfɪk will try to find a `traefik.toml` in the following places:
- `/etc/traefik/`
- `$HOME/.traefik/`
- `.` the working directory
You can override this by setting a `configFile` argument:
```bash
$ traefik --configFile=foo/bar/myconfigfile.toml
```
Træfɪk uses the following precedence order. Each item takes precedence over the item below it:
- arguments
- configuration file
- default
It means that arguments overrides configuration file.
Each argument is described in the help section:
```bash
$ traefik --help
traefik is a modern HTTP reverse proxy and load balancer made to deploy microservices with ease.
Complete documentation is available at http://traefik.io
Usage:
traefik [flags]
traefik [command]
Available Commands:
version Print version
Flags:
--accessLogsFile string Access logs file (default "log/access.log")
--boltdb Enable Boltdb backend
--boltdb.endpoint string Boltdb server endpoint (default "127.0.0.1:4001")
--boltdb.filename string Override default configuration template. For advanced users :)
--boltdb.prefix string Prefix used for KV store (default "/traefik")
--boltdb.watch Watch provider (default true)
-c, --configFile string Configuration file to use (TOML, JSON, YAML, HCL).
--consul Enable Consul backend
--consul.endpoint string Consul server endpoint (default "127.0.0.1:8500")
--consul.filename string Override default configuration template. For advanced users :)
--consul.prefix string Prefix used for KV store (default "/traefik")
--consul.watch Watch provider (default true)
--consulCatalog Enable Consul catalog backend
--consulCatalog.domain string Default domain used
--consulCatalog.endpoint string Consul server endpoint (default "127.0.0.1:8500")
--defaultEntryPoints value Entrypoints to be used by frontends that do not specify any entrypoint (default &main.DefaultEntryPoints(nil))
--docker Enable Docker backend
--docker.domain string Default domain used
--docker.endpoint string Docker server endpoint. Can be a tcp or a unix socket endpoint (default "unix:///var/run/docker.sock")
--docker.filename string Override default configuration template. For advanced users :)
--docker.tls Enable Docker TLS support
--docker.tls.ca string TLS CA
--docker.tls.cert string TLS cert
--docker.tls.insecureSkipVerify TLS insecure skip verify
--docker.tls.key string TLS key
--docker.watch Watch provider (default true)
--entryPoints value Entrypoints definition using format: --entryPoints='Name:http Address::8000 Redirect.EntryPoint:https' --entryPoints='Name:https Address::4442 TLS:tests/traefik.crt,tests/traefik.key'
--etcd Enable Etcd backend
--etcd.endpoint string Etcd server endpoint (default "127.0.0.1:4001")
--etcd.filename string Override default configuration template. For advanced users :)
--etcd.prefix string Prefix used for KV store (default "/traefik")
--etcd.watch Watch provider (default true)
--file Enable File backend
--file.filename string Override default configuration template. For advanced users :)
--file.watch Watch provider (default true)
-g, --graceTimeOut string Timeout in seconds. Duration to give active requests a chance to finish during hot-reloads (default "10")
-l, --logLevel string Log level (default "ERROR")
--marathon Enable Marathon backend
--marathon.domain string Default domain used
--marathon.endpoint string Marathon server endpoint. You can also specify multiple endpoint for Marathon (default "http://127.0.0.1:8080")
--marathon.filename string Override default configuration template. For advanced users :)
--marathon.networkInterface string Network interface used to call Marathon web services. Needed in case of multiple network interfaces (default "eth0")
--marathon.watch Watch provider (default true)
--maxIdleConnsPerHost int If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used
-p, --port string Reverse proxy port (default ":80")
--providersThrottleDuration duration Backends throttle duration: minimum duration between 2 events from providers before applying a new configuration. It avoids unnecessary reloads if multiples events are sent in a short amount of time. (default 2s)
--traefikLogsFile string Traefik logs file (default "log/traefik.log")
--web Enable Web backend
--web.address string Web administration port (default ":8080")
--web.cerFile string SSL certificate
--web.keyFile string SSL certificate
--web.readOnly Enable read only API
--zookeeper Enable Zookeeper backend
--zookeeper.endpoint string Zookeeper server endpoint (default "127.0.0.1:2181")
--zookeeper.filename string Override default configuration template. For advanced users :)
--zookeeper.prefix string Prefix used for KV store (default "/traefik")
--zookeeper.watch Watch provider (default true)
Use "traefik [command] --help" for more information about a command.
```
## <a id="global"></a> Global configuration
```toml
@@ -67,12 +167,45 @@ For example:
# Global configuration
################################################################
# Reverse proxy port
# Entrypoints definition
#
# Optional
# Default: ":80"
# Default:
# [entryPoints]
# [entryPoints.http]
# address = ":80"
#
# port = ":80"
# To redirect an http entrypoint to an https entrypoint (with SNI support):
# [entryPoints]
# [entryPoints.http]
# address = ":80"
# [entryPoints.http.redirect]
# entryPoint = "https"
# [entryPoints.https]
# address = ":443"
# [entryPoints.https.tls]
# [[entryPoints.https.tls.certificates]]
# CertFile = "integration/fixtures/https/snitest.com.cert"
# KeyFile = "integration/fixtures/https/snitest.com.key"
# [[entryPoints.https.tls.certificates]]
# CertFile = "integration/fixtures/https/snitest.org.cert"
# KeyFile = "integration/fixtures/https/snitest.org.key"
#
# To redirect an entrypoint rewriting the URL:
# [entryPoints]
# [entryPoints.http]
# address = ":80"
# [entryPoints.http.redirect]
# regex = "^http://localhost/(.*)"
# replacement = "http://mydomain/$1"
# Entrypoints to be used by frontends that do not specify any entrypoint.
# Each frontend can specify its own entrypoints.
#
# Optional
# Default: ["http"]
#
# defaultEntryPoints = ["http", "https"]
# Timeout in seconds.
# Duration to give active requests a chance to finish during hot-reloads
@@ -102,15 +235,6 @@ For example:
#
# logLevel = "ERROR"
# SSL certificates and keys
# You may add several certificate/key pairs to terminate HTTPS for multiple domain names using TLS SNI
#
# Optional
#
# [[certificates]]
# CertFile = "traefik.crt"
# KeyFile = "traefik.key"
# Backends throttle duration: minimum duration between 2 events from providers
# before applying a new configuration. It avoids unnecessary reloads if multiples events
# are sent in a short amount of time.
@@ -120,6 +244,14 @@ For example:
#
# ProvidersThrottleDuration = "5s"
# If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used.
# If you encounter 'too many open files' errors, you can either change this value, or change `ulimit` value.
#
# Optional
# Default: http.DefaultMaxIdleConnsPerHost
#
# MaxIdleConnsPerHost = 200
```
@@ -131,7 +263,21 @@ Like any other reverse proxy, Træfɪk can be configured with a file. You have t
```toml
# traefik.toml
port = ":80"
defaultEntryPoints = ["http", "https"]
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.http.redirect]
entryPoint = "https"
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
[[entryPoints.https.tls.certificates]]
CertFile = "integration/fixtures/https/snitest.com.cert"
KeyFile = "integration/fixtures/https/snitest.com.key"
[[entryPoints.https.tls.certificates]]
CertFile = "integration/fixtures/https/snitest.org.cert"
KeyFile = "integration/fixtures/https/snitest.org.key"
graceTimeOut = 10
logLevel = "DEBUG"
@@ -167,7 +313,13 @@ logLevel = "DEBUG"
[frontends.frontend2]
backend = "backend1"
passHostHeader = true
[frontends.frontend2.routes.test_2]
entrypoints = ["https"] # overrides defaultEntryPoints
[frontends.frontend2.routes.test_1]
rule = "Host"
value = "{subdomain:[a-z]+}.localhost"
[frontends.frontend3]
entrypoints = ["http", "https"] # overrides defaultEntryPoints
backend = "backend2"
rule = "Path"
value = "/test"
```
@@ -176,7 +328,20 @@ logLevel = "DEBUG"
```toml
# traefik.toml
port = ":80"
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.http.redirect]
entryPoint = "https"
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
[[entryPoints.https.tls.certificates]]
CertFile = "integration/fixtures/https/snitest.com.cert"
KeyFile = "integration/fixtures/https/snitest.com.key"
[[entryPoints.https.tls.certificates]]
CertFile = "integration/fixtures/https/snitest.org.cert"
KeyFile = "integration/fixtures/https/snitest.org.key"
graceTimeOut = 10
logLevel = "DEBUG"
@@ -215,10 +380,15 @@ filename = "rules.toml"
[frontends.frontend2]
backend = "backend1"
passHostHeader = true
[frontends.frontend2.routes.test_2]
entrypoints = ["https"] # overrides defaultEntryPoints
[frontends.frontend2.routes.test_1]
rule = "Host"
value = "{subdomain:[a-z]+}.localhost"
[frontends.frontend3]
entrypoints = ["http", "https"] # overrides defaultEntryPoints
backend = "backend2"
rule = "Path"
value = "/test"
```
If you want Træfɪk to watch file changes automatically, just add:
@@ -429,8 +599,9 @@ Labels can be used on containers to override default behaviour:
- `traefik.weight=10`: assign this weight to the container
- `traefik.enable=false`: disable this container in Træfɪk
- `traefik.frontend.rule=Host`: override the default frontend rule (Default: Host). See [frontends](#frontends).
- `traefik.frontend.value=test.example.com`: override the default frontend value (Default: `{containerName}.{domain}`) See [frontends](#frontends).
- `traefik.frontend.value=test.example.com`: override the default frontend value (Default: `{containerName}.{domain}`) See [frontends](#frontends). Must be associated with label traefik.frontend.rule.
- `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend.
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
* `traefik.domain=traefik.localhost`: override the default domain
@@ -458,11 +629,11 @@ Træfɪk can be configured to use Marathon as a backend configuration:
#
endpoint = "http://127.0.0.1:8080"
# Network interface used to call Marathon web services
# Network interface used to call Marathon web services. Needed in case of multiple network interfaces.
# Optional
# Default: "eth0"
#
# networkInterface = "eth0"
networkInterface = "eth0"
# Enable watch Marathon changes
#
@@ -482,6 +653,21 @@ domain = "marathon.localhost"
# Optional
#
# filename = "marathon.tmpl"
# Enable Marathon basic authentication
#
# Optional
#
# [marathon.basic]
# httpBasicAuthUser = "foo"
# httpBasicPassword = "bar"
# TLS client configuration. https://golang.org/pkg/crypto/tls/#Config
#
# Optional
#
# [marathon.TLS]
# InsecureSkipVerify = true
```
Labels can be used on containers to override default behaviour:
@@ -493,8 +679,9 @@ Labels can be used on containers to override default behaviour:
- `traefik.weight=10`: assign this weight to the application
- `traefik.enable=false`: disable this application in Træfɪk
- `traefik.frontend.rule=Host`: override the default frontend rule (Default: Host). See [frontends](#frontends).
- `traefik.frontend.value=test.example.com`: override the default frontend value (Default: `{appName}.{domain}`) See [frontends](#frontends).
- `traefik.frontend.value=test.example.com`: override the default frontend value (Default: `{appName}.{domain}`) See [frontends](#frontends). Must be associated with label traefik.frontend.rule.
- `traefik.frontend.passHostHeader=true`: forward client `Host` header to the backend.
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
* `traefik.domain=traefik.localhost`: override the default domain
## <a id="consul"></a> Consul backend
@@ -573,6 +760,7 @@ The Keys-Values structure should look (using `prefix = "/traefik"`):
|----------------------------------------------------|------------|
| `/traefik/frontends/frontend2/backend` | `backend1` |
| `/traefik/frontends/frontend2/passHostHeader` | `true` |
| `/traefik/frontends/frontend2/entrypoints` |`http,https`|
| `/traefik/frontends/frontend2/routes/test_2/rule` | `Path` |
| `/traefik/frontends/frontend2/routes/test_2/value` | `/test` |
@@ -653,10 +841,42 @@ The Keys-Values structure should look (using `prefix = "/traefik"`):
|----------------------------------------------------|------------|
| `/traefik/frontends/frontend2/backend` | `backend1` |
| `/traefik/frontends/frontend2/passHostHeader` | `true` |
| `/traefik/frontends/frontend2/entrypoints` |`http,https`|
| `/traefik/frontends/frontend2/routes/test_2/rule` | `Path` |
| `/traefik/frontends/frontend2/routes/test_2/value` | `/test` |
## <a id="consulcatalog"></a> Consul catalog backend
Træfɪk can be configured to use service discovery catalog of Consul as a backend configuration:
```toml
################################################################
# Consul Catalog configuration backend
################################################################
# Enable Consul Catalog configuration backend
#
# Optional
#
[consulCatalog]
# Consul server endpoint
#
# Required
#
endpoint = "127.0.0.1:8500"
# Default domain used.
#
# Optional
#
domain = "consul.localhost"
```
This backend will create routes matching on hostname based on the service name
used in consul.
## <a id="zk"></a> Zookeeper backend
Træfɪk can be configured to use Zookeeper as a backend configuration:
@@ -732,6 +952,7 @@ The Keys-Values structure should look (using `prefix = "/traefik"`):
|----------------------------------------------------|------------|
| `/traefik/frontends/frontend2/backend` | `backend1` |
| `/traefik/frontends/frontend2/passHostHeader` | `true` |
| `/traefik/frontends/frontend2/entrypoints` |`http,https`|
| `/traefik/frontends/frontend2/routes/test_2/rule` | `Path` |
| `/traefik/frontends/frontend2/routes/test_2/value` | `/test` |
@@ -776,6 +997,44 @@ Træfɪk can be configured to use BoltDB as a backend configuration:
# filename = "boltdb.tmpl"
```
## <a id="atomicconfig"></a> Atomic configuration changes
The [Etcd](https://github.com/coreos/etcd/issues/860) and [Consul](https://github.com/hashicorp/consul/issues/886) backends do not support updating multiple keys atomically. As a result, it may be possible for Træfɪk to read an intermediate configuration state despite judicious use of the `--providersThrottleDuration` flag. To solve this problem, Træfɪk supports a special key called `/traefik/alias`. If set, Træfɪk use the value as an alternative key prefix.
Given the key structure below, Træfɪk will use the `http://172.17.0.2:80` as its only backend (frontend keys have been omitted for brevity).
| Key | Value |
|-------------------------------------------------------------------------|-----------------------------|
| `/traefik/alias` | `/traefik_configurations/1` |
| `/traefik_configurations/1/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
| `/traefik_configurations/1/backends/backend1/servers/server1/weight` | `10` |
When an atomic configuration change is required, you may write a new configuration at an alternative prefix. Here, although the `/traefik_configurations/2/...` keys have been set, the old configuration is still active because the `/traefik/alias` key still points to `/traefik_configurations/1`:
| Key | Value |
|-------------------------------------------------------------------------|-----------------------------|
| `/traefik/alias` | `/traefik_configurations/1` |
| `/traefik_configurations/1/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
| `/traefik_configurations/1/backends/backend1/servers/server1/weight` | `10` |
| `/traefik_configurations/2/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
| `/traefik_configurations/2/backends/backend1/servers/server1/weight` | `5` |
| `/traefik_configurations/2/backends/backend1/servers/server2/url` | `http://172.17.0.3:80` |
| `/traefik_configurations/2/backends/backend1/servers/server2/weight` | `5` |
Once the `/traefik/alias` key is updated, the new `/traefik_configurations/2` configuration becomes active atomically. Here, we have a 50% balance between the `http://172.17.0.3:80` and the `http://172.17.0.4:80` hosts while no traffic is sent to the `172.17.0.2:80` host:
| Key | Value |
|-------------------------------------------------------------------------|-----------------------------|
| `/traefik/alias` | `/traefik_configurations/2` |
| `/traefik_configurations/1/backends/backend1/servers/server1/url` | `http://172.17.0.2:80` |
| `/traefik_configurations/1/backends/backend1/servers/server1/weight` | `10` |
| `/traefik_configurations/2/backends/backend1/servers/server1/url` | `http://172.17.0.3:80` |
| `/traefik_configurations/2/backends/backend1/servers/server1/weight` | `5` |
| `/traefik_configurations/2/backends/backend1/servers/server2/url` | `http://172.17.0.4:80` |
| `/traefik_configurations/2/backends/backend1/servers/server2/weight` | `5` |
Note that Træfɪk *will not watch for key changes in the `/traefik_configurations` prefix*. It will only watch for changes in the `/traefik` prefix. Further, if the `/traefik/alias` key is set, all other sibling keys with the `/traefik` prefix are ignored.
## <a id="benchmarks"></a> Benchmarks
@@ -846,9 +1105,9 @@ Percentage of the requests served within a certain time (ms)
- Træfɪk:
```sh
$ docker run -d -l traefik.backend=test1 -l traefik.host=test1 emilevauge/whoami
$ docker run -d -l traefik.backend=test1 -l traefik.host=test1 emilevauge/whoami
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/traefik.toml -v /var/run/docker.sock:/var/run/docker.sock emilevauge/traefik
docker run -d -l traefik.backend=test1 -l traefik.frontend.rule=Host -l traefik.frontend.value=test1.docker.localhost emilevauge/whoami
docker run -d -l traefik.backend=test1 -l traefik.frontend.rule=Host -l traefik.frontend.value=test1.docker.localhost emilevauge/whoami
docker run -d -p 8080:8080 -p 80:80 -v $PWD/traefik.toml:/traefik.toml -v /var/run/docker.sock:/var/run/docker.sock containous/traefik
$ ab -n 20000 -c 20 -r http://test1.docker.localhost/
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/

View File

@@ -43,7 +43,7 @@ import:
- package: github.com/alecthomas/units
ref: 6b4e7dc5e3143b85ea77909c72caf89416fc2915
- package: github.com/gambol99/go-marathon
ref: 8ce3f764250b2de3f2c627d12ca7dd21bd5e7f93
ref: ade11d1dc2884ee1f387078fc28509559b6235d1
- package: github.com/mailgun/predicate
ref: cb0bff91a7ab7cf7571e661ff883fc997bc554a3
- package: github.com/thoas/stats
@@ -124,7 +124,7 @@ import:
- package: gopkg.in/alecthomas/kingpin.v2
ref: 639879d6110b1b0409410c7b737ef0bb18325038
- package: github.com/docker/libcompose
ref: 79ef5d150f053a5b12f16b02d8844ed7cf33611a
ref: d3089811c119a211469a9cc93caea684d937e5d3
subpackages:
- docker
- logger
@@ -146,4 +146,22 @@ import:
ref: d8a3071799b98cacd30b6da92f536050ccfe6da4
- package: github.com/golang/glog
ref: fca8c8854093a154ff1eb580aae10276ad6b1b5f
- package: github.com/spf13/cast
ref: ee7b3e0353166ab1f3a605294ac8cd2b77953778
- package: github.com/mitchellh/mapstructure
- package: github.com/spf13/jwalterweatherman
- package: github.com/spf13/pflag
- package: github.com/wendal/errors
- package: github.com/hashicorp/hcl
- package: github.com/kr/pretty
- package: github.com/magiconair/properties
- package: github.com/kr/text
- package: github.com/spf13/viper
ref: a212099cbe6fbe8d07476bfda8d2d39b6ff8f325
- package: github.com/spf13/cobra
subpackages:
- /cobra
- package: github.com/google/go-querystring/query
- package: github.com/vulcand/vulcand/plugin/rewrite
- package: github.com/stretchr/testify/mock

View File

@@ -1,11 +1,11 @@
package main
import (
"fmt"
"net/http"
"os/exec"
"time"
"fmt"
checker "github.com/vdemeester/shakers"
check "gopkg.in/check.v1"
)
@@ -18,10 +18,10 @@ func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) {
output, err := cmd.CombinedOutput()
c.Assert(err, checker.NotNil)
c.Assert(string(output), checker.Contains, "Error reading file: open traefik.toml: no such file or directory")
c.Assert(string(output), checker.Contains, "Error reading file: open : no such file or directory")
nonExistentFile := "non/existent/file.toml"
cmd = exec.Command(traefikBinary, nonExistentFile)
cmd = exec.Command(traefikBinary, "--configFile="+nonExistentFile)
output, err = cmd.CombinedOutput()
c.Assert(err, checker.NotNil)
@@ -29,30 +29,31 @@ func (s *SimpleSuite) TestNoOrInexistentConfigShouldFail(c *check.C) {
}
func (s *SimpleSuite) TestInvalidConfigShouldFail(c *check.C) {
cmd := exec.Command(traefikBinary, "fixtures/invalid_configuration.toml")
cmd := exec.Command(traefikBinary, "--configFile=fixtures/invalid_configuration.toml")
output, err := cmd.CombinedOutput()
c.Assert(err, checker.NotNil)
c.Assert(string(output), checker.Contains, "Error reading file: Near line 1")
c.Assert(string(output), checker.Contains, "Error reading file: While parsing config: Near line 1")
}
func (s *SimpleSuite) TestSimpleDefaultConfig(c *check.C) {
cmd := exec.Command(traefikBinary, "fixtures/simple_default.toml")
cmd := exec.Command(traefikBinary, "--configFile=fixtures/simple_default.toml")
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
time.Sleep(500 * time.Millisecond)
// TODO validate : run on 80
resp, err := http.Get("http://127.0.0.1/")
resp, err := http.Get("http://127.0.0.1:8000/")
// Expected a 404 as we did not comfigure anything
c.Assert(err, checker.IsNil)
c.Assert(resp.StatusCode, checker.Equals, 404)
// Expected no response as we did not configure anything
c.Assert(resp, checker.IsNil)
c.Assert(err, checker.NotNil)
c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused"))
}
func (s *SimpleSuite) TestWithWebConfig(c *check.C) {
cmd := exec.Command(traefikBinary, "fixtures/simple_web.toml")
cmd := exec.Command(traefikBinary, "--configFile=fixtures/simple_web.toml")
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()

View File

@@ -0,0 +1,132 @@
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"time"
"github.com/docker/docker/opts"
"github.com/fsouza/go-dockerclient"
"github.com/hashicorp/consul/api"
checker "github.com/vdemeester/shakers"
check "gopkg.in/check.v1"
)
// Consul catalog test suites
type ConsulCatalogSuite struct {
BaseSuite
consulIP string
consulClient *api.Client
dockerClient *docker.Client
}
func (s *ConsulCatalogSuite) GetContainer(name string) (*docker.Container, error) {
return s.dockerClient.InspectContainer(name)
}
func (s *ConsulCatalogSuite) SetUpSuite(c *check.C) {
dockerHost := os.Getenv("DOCKER_HOST")
if dockerHost == "" {
// FIXME Handle windows -- see if dockerClient already handle that or not
dockerHost = fmt.Sprintf("unix://%s", opts.DefaultUnixSocket)
}
// Make sure we can speak to docker
dockerClient, err := docker.NewClient(dockerHost)
c.Assert(err, checker.IsNil, check.Commentf("Error connecting to docker daemon"))
s.dockerClient = dockerClient
s.createComposeProject(c, "consul_catalog")
err = s.composeProject.Up()
c.Assert(err, checker.IsNil, check.Commentf("Error starting project"))
consul, err := s.GetContainer("integration-test-consul_catalog_consul_1")
c.Assert(err, checker.IsNil, check.Commentf("Error finding consul container"))
s.consulIP = consul.NetworkSettings.IPAddress
config := api.DefaultConfig()
config.Address = s.consulIP + ":8500"
consulClient, err := api.NewClient(config)
if err != nil {
c.Fatalf("Error creating consul client")
}
s.consulClient = consulClient
// Wait for consul to elect itself leader
time.Sleep(2000 * time.Millisecond)
}
func (s *ConsulCatalogSuite) registerService(name string, address string, port int) error {
catalog := s.consulClient.Catalog()
_, err := catalog.Register(
&api.CatalogRegistration{
Node: address,
Address: address,
Service: &api.AgentService{
ID: name,
Service: name,
Address: address,
Port: port,
},
},
&api.WriteOptions{},
)
return err
}
func (s *ConsulCatalogSuite) deregisterService(name string, address string) error {
catalog := s.consulClient.Catalog()
_, err := catalog.Deregister(
&api.CatalogDeregistration{
Node: address,
Address: address,
ServiceID: name,
},
&api.WriteOptions{},
)
return err
}
func (s *ConsulCatalogSuite) TestSimpleConfiguration(c *check.C) {
cmd := exec.Command(traefikBinary, "--consulCatalog", "--consulCatalog.endpoint="+s.consulIP+":8500", "--configFile=fixtures/consul_catalog/simple.toml")
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
time.Sleep(500 * time.Millisecond)
// TODO validate : run on 80
resp, err := http.Get("http://127.0.0.1:8000/")
// Expected a 404 as we did not configure anything
c.Assert(err, checker.IsNil)
c.Assert(resp.StatusCode, checker.Equals, 404)
}
func (s *ConsulCatalogSuite) TestSingleService(c *check.C) {
cmd := exec.Command(traefikBinary, "--consulCatalog", "--consulCatalog.endpoint="+s.consulIP+":8500", "--consulCatalog.domain=consul.localhost", "--configFile=fixtures/consul_catalog/simple.toml")
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
nginx, err := s.GetContainer("integration-test-consul_catalog_nginx_1")
c.Assert(err, checker.IsNil, check.Commentf("Error finding nginx container"))
err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80)
c.Assert(err, checker.IsNil, check.Commentf("Error registering service"))
defer s.deregisterService("test", nginx.NetworkSettings.IPAddress)
time.Sleep(5000 * time.Millisecond)
client := &http.Client{}
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
c.Assert(err, checker.IsNil)
req.Host = "test.consul.localhost"
resp, err := client.Do(req)
c.Assert(err, checker.IsNil)
c.Assert(resp.StatusCode, checker.Equals, 200)
_, err = ioutil.ReadAll(resp.Body)
c.Assert(err, checker.IsNil)
}

View File

@@ -5,21 +5,23 @@ import (
"os/exec"
"time"
"fmt"
checker "github.com/vdemeester/shakers"
check "gopkg.in/check.v1"
)
func (s *ConsulSuite) TestSimpleConfiguration(c *check.C) {
cmd := exec.Command(traefikBinary, "fixtures/consul/simple.toml")
cmd := exec.Command(traefikBinary, "--configFile=fixtures/consul/simple.toml")
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
time.Sleep(500 * time.Millisecond)
// TODO validate : run on 80
resp, err := http.Get("http://127.0.0.1/")
resp, err := http.Get("http://127.0.0.1:8000/")
// Expected a 404 as we did not comfigure anything
c.Assert(err, checker.IsNil)
c.Assert(resp.StatusCode, checker.Equals, 404)
// Expected no response as we did not configure anything
c.Assert(resp, checker.IsNil)
c.Assert(err, checker.NotNil)
c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused"))
}

View File

@@ -139,14 +139,14 @@ func (s *DockerSuite) TestSimpleConfiguration(c *check.C) {
file := s.adaptFileForHost(c, "fixtures/docker/simple.toml")
defer os.Remove(file)
cmd := exec.Command(traefikBinary, file)
cmd := exec.Command(traefikBinary, "--configFile="+file)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
time.Sleep(500 * time.Millisecond)
// TODO validate : run on 80
resp, err := http.Get("http://127.0.0.1/")
resp, err := http.Get("http://127.0.0.1:8000/")
c.Assert(err, checker.IsNil)
// Expected a 404 as we did not comfigure anything
@@ -159,7 +159,7 @@ func (s *DockerSuite) TestDefaultDockerContainers(c *check.C) {
name := s.startContainer(c, "swarm:1.0.0", "manage", "token://blablabla")
// Start traefik
cmd := exec.Command(traefikBinary, file)
cmd := exec.Command(traefikBinary, "--configFile="+file)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
@@ -168,7 +168,7 @@ func (s *DockerSuite) TestDefaultDockerContainers(c *check.C) {
time.Sleep(1500 * time.Millisecond)
client := &http.Client{}
req, err := http.NewRequest("GET", "http://127.0.0.1/version", nil)
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/version", nil)
c.Assert(err, checker.IsNil)
req.Host = fmt.Sprintf("%s.docker.localhost", name)
resp, err := client.Do(req)
@@ -196,7 +196,7 @@ func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) {
s.startContainerWithLabels(c, "swarm:1.0.0", labels, "manage", "token://blabla")
// Start traefik
cmd := exec.Command(traefikBinary, file)
cmd := exec.Command(traefikBinary, "--configFile="+file)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
@@ -205,7 +205,7 @@ func (s *DockerSuite) TestDockerContainersWithLabels(c *check.C) {
time.Sleep(1500 * time.Millisecond)
client := &http.Client{}
req, err := http.NewRequest("GET", "http://127.0.0.1/version", nil)
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/version", nil)
c.Assert(err, checker.IsNil)
req.Host = fmt.Sprintf("my.super.host")
resp, err := client.Do(req)
@@ -232,7 +232,7 @@ func (s *DockerSuite) TestDockerContainersWithOneMissingLabels(c *check.C) {
s.startContainerWithLabels(c, "swarm:1.0.0", labels, "manage", "token://blabla")
// Start traefik
cmd := exec.Command(traefikBinary, file)
cmd := exec.Command(traefikBinary, "--configFile="+file)
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
@@ -241,7 +241,7 @@ func (s *DockerSuite) TestDockerContainersWithOneMissingLabels(c *check.C) {
time.Sleep(1500 * time.Millisecond)
client := &http.Client{}
req, err := http.NewRequest("GET", "http://127.0.0.1/version", nil)
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/version", nil)
c.Assert(err, checker.IsNil)
req.Host = fmt.Sprintf("my.super.host")
resp, err := client.Do(req)

View File

@@ -10,13 +10,13 @@ import (
)
func (s *FileSuite) TestSimpleConfiguration(c *check.C) {
cmd := exec.Command(traefikBinary, "fixtures/file/simple.toml")
cmd := exec.Command(traefikBinary, "--configFile=fixtures/file/simple.toml")
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
time.Sleep(1000 * time.Millisecond)
resp, err := http.Get("http://127.0.0.1/")
resp, err := http.Get("http://127.0.0.1:8000/")
// Expected a 404 as we did not configure anything
c.Assert(err, checker.IsNil)
@@ -25,13 +25,13 @@ func (s *FileSuite) TestSimpleConfiguration(c *check.C) {
// #56 regression test, make sure it does not fail
func (s *FileSuite) TestSimpleConfigurationNoPanic(c *check.C) {
cmd := exec.Command(traefikBinary, "fixtures/file/56-simple-panic.toml")
cmd := exec.Command(traefikBinary, "--configFile=fixtures/file/56-simple-panic.toml")
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
time.Sleep(1000 * time.Millisecond)
resp, err := http.Get("http://127.0.0.1/")
resp, err := http.Get("http://127.0.0.1:8000/")
// Expected a 404 as we did not configure anything
c.Assert(err, checker.IsNil)

View File

@@ -1,11 +1,9 @@
# Reverse proxy port
#
# Optional
# Default: ":80"
#
# port = ":80"
#
# LogLevel
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":8000"
logLevel = "DEBUG"
[consul]

View File

@@ -0,0 +1,9 @@
defaultEntryPoints = ["http"]
logLevel = "DEBUG"
[entryPoints]
[entryPoints.http]
address = ":8000"
[consulCatalog]
domain = "consul.localhost"

View File

@@ -1,11 +1,9 @@
# Reverse proxy port
#
# Optional
# Default: ":80"
#
# port = ":80"
#
# LogLevel
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":8000"
logLevel = "DEBUG"
[docker]

View File

@@ -1,11 +1,9 @@
# Reverse proxy port
#
# Optional
# Default: ":80"
#
# port = ":80"
#
# LogLevel
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":8000"
logLevel = "DEBUG"
[file]

View File

@@ -1,11 +1,9 @@
# Reverse proxy port
#
# Optional
# Default: ":80"
#
# port = ":80"
#
# LogLevel
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":8000"
logLevel = "DEBUG"
[file]

View File

@@ -1,13 +1,17 @@
port = ":443"
logLevel = "DEBUG"
[[certificates]]
CertFile = "fixtures/https/snitest.com.cert"
KeyFile = "fixtures/https/snitest.com.key"
defaultEntryPoints = ["https"]
[[certificates]]
CertFile = "fixtures/https/snitest.org.cert"
KeyFile = "fixtures/https/snitest.org.key"
[entryPoints]
[entryPoints.https]
address = ":4443"
[entryPoints.https.tls]
[[entryPoints.https.tls.certificates]]
CertFile = "fixtures/https/snitest.com.cert"
KeyFile = "fixtures/https/snitest.com.key"
[[entryPoints.https.tls.certificates]]
CertFile = "fixtures/https/snitest.org.cert"
KeyFile = "fixtures/https/snitest.org.key"
[file]

View File

@@ -1,11 +1,9 @@
# Reverse proxy port
#
# Optional
# Default: ":80"
#
# port = ":80"
#
# LogLevel
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":8000"
logLevel = "DEBUG"
[marathon]

View File

@@ -1,9 +1,5 @@
# Reverse proxy port
#
# Optional
# Default: ":80"
#
# port = ":80"
#
# LogLevel
logLevel = "DEBUG"
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":8000"

View File

@@ -1,5 +1,9 @@
logLevel = "DEBUG"
defaultEntryPoints = ["http"]
[entryPoints]
[entryPoints.http]
address = ":8000"
[web]
address = ":8080"

View File

@@ -19,7 +19,7 @@ type HTTPSSuite struct{ BaseSuite }
// "snitest.com", which happens to match the CN of 'snitest.com.crt'. The test
// verifies that traefik presents the correct certificate.
func (s *HTTPSSuite) TestWithSNIConfigHandshake(c *check.C) {
cmd := exec.Command(traefikBinary, "fixtures/https/https_sni.toml")
cmd := exec.Command(traefikBinary, "--configFile=fixtures/https/https_sni.toml")
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
@@ -30,7 +30,7 @@ func (s *HTTPSSuite) TestWithSNIConfigHandshake(c *check.C) {
InsecureSkipVerify: true,
ServerName: "snitest.com",
}
conn, err := tls.Dial("tcp", "127.0.0.1:443", tlsConfig)
conn, err := tls.Dial("tcp", "127.0.0.1:4443", tlsConfig)
c.Assert(err, checker.IsNil, check.Commentf("failed to connect to server"))
defer conn.Close()
@@ -46,7 +46,7 @@ func (s *HTTPSSuite) TestWithSNIConfigHandshake(c *check.C) {
// SNI hostnames of "snitest.org" and "snitest.com". The test verifies
// that traefik routes the requests to the expected backends.
func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) {
cmd := exec.Command(traefikBinary, "fixtures/https/https_sni.toml")
cmd := exec.Command(traefikBinary, "--configFile=fixtures/https/https_sni.toml")
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
@@ -72,7 +72,7 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) {
}
client := &http.Client{Transport: tr1}
req, _ := http.NewRequest("GET", "https://127.0.0.1/", nil)
req, _ := http.NewRequest("GET", "https://127.0.0.1:4443/", nil)
req.Host = "snitest.com"
req.Header.Set("Host", "snitest.com")
req.Header.Set("Accept", "*/*")
@@ -82,7 +82,7 @@ func (s *HTTPSSuite) TestWithSNIConfigRoute(c *check.C) {
c.Assert(resp.StatusCode, checker.Equals, 204)
client = &http.Client{Transport: tr2}
req, _ = http.NewRequest("GET", "https://127.0.0.1/", nil)
req, _ = http.NewRequest("GET", "https://127.0.0.1:4443/", nil)
req.Host = "snitest.org"
req.Header.Set("Host", "snitest.org")
req.Header.Set("Accept", "*/*")

View File

@@ -10,9 +10,9 @@ import (
"testing"
"text/template"
"github.com/containous/traefik/integration/utils"
"github.com/docker/libcompose/docker"
"github.com/docker/libcompose/project"
"github.com/emilevauge/traefik/integration/utils"
checker "github.com/vdemeester/shakers"
check "gopkg.in/check.v1"
@@ -28,6 +28,7 @@ func init() {
check.Suite(&FileSuite{})
check.Suite(&DockerSuite{})
check.Suite(&ConsulSuite{})
check.Suite(&ConsulCatalogSuite{})
check.Suite(&MarathonSuite{})
}
@@ -80,13 +81,18 @@ func (s *BaseSuite) TearDownSuite(c *check.C) {
func (s *BaseSuite) createComposeProject(c *check.C, name string) {
composeProject, err := docker.NewProject(&docker.Context{
Context: project.Context{
ComposeFile: fmt.Sprintf("resources/compose/%s.yml", name),
ComposeFiles: []string{
fmt.Sprintf("resources/compose/%s.yml", name),
},
ProjectName: fmt.Sprintf("integration-test-%s", name),
},
})
c.Assert(err, checker.IsNil)
s.composeProject = composeProject
err = composeProject.Create()
c.Assert(err, checker.IsNil)
s.started = make(chan bool)
s.stopped = make(chan bool)
s.deleted = make(chan bool)
@@ -96,7 +102,8 @@ func (s *BaseSuite) createComposeProject(c *check.C, name string) {
composeProject.AddListener(s.listenChan)
composeProject.Start()
err = composeProject.Start()
c.Assert(err, checker.IsNil)
// Wait for compose to start
<-s.started

View File

@@ -1,6 +1,7 @@
package main
import (
"fmt"
"net/http"
"os/exec"
"time"
@@ -10,16 +11,17 @@ import (
)
func (s *MarathonSuite) TestSimpleConfiguration(c *check.C) {
cmd := exec.Command(traefikBinary, "fixtures/marathon/simple.toml")
cmd := exec.Command(traefikBinary, "--configFile=fixtures/marathon/simple.toml")
err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()
time.Sleep(500 * time.Millisecond)
// TODO validate : run on 80
resp, err := http.Get("http://127.0.0.1/")
resp, err := http.Get("http://127.0.0.1:8000/")
// Expected a 404 as we did not configure anything
c.Assert(err, checker.IsNil)
c.Assert(resp.StatusCode, checker.Equals, 404)
// Expected no response as we did not configure anything
c.Assert(resp, checker.IsNil)
c.Assert(err, checker.NotNil)
c.Assert(err.Error(), checker.Contains, fmt.Sprintf("getsockopt: connection refused"))
}

View File

@@ -0,0 +1,17 @@
consul:
image: progrium/consul
command: -server -bootstrap -log-level debug -ui-dir /ui
ports:
- "8400:8400"
- "8500:8500"
- "8600:53/udp"
expose:
- "8300"
- "8301"
- "8301/udp"
- "8302"
- "8302/udp"
nginx:
image: nginx
ports:
- "8881:80"

View File

@@ -3,7 +3,7 @@ zk:
net: host
environment:
ZK_CONFIG: tickTime=2000,initLimit=10,syncLimit=5,maxClientCnxns=128,forceSync=no,clientPort=2181
ZK_ID: 1
ZK_ID: " 1"
master:
image: mesosphere/mesos-master:0.23.0-1.0.ubuntu1404
@@ -12,7 +12,7 @@ master:
MESOS_ZK: zk://127.0.0.1:2181/mesos
MESOS_HOSTNAME: 127.0.0.1
MESOS_IP: 127.0.0.1
MESOS_QUORUM: 1
MESOS_QUORUM: " 1"
MESOS_CLUSTER: docker-compose
MESOS_WORK_DIR: /var/lib/mesos

31
middlewares/rewrite.go Normal file
View File

@@ -0,0 +1,31 @@
package middlewares
import (
log "github.com/Sirupsen/logrus"
"github.com/vulcand/vulcand/plugin/rewrite"
"net/http"
)
// Rewrite is a middleware that allows redirections
type Rewrite struct {
rewriter *rewrite.Rewrite
}
// NewRewrite creates a Rewrite middleware
func NewRewrite(regex, replacement string, redirect bool) (*Rewrite, error) {
rewriter, err := rewrite.NewRewrite(regex, replacement, false, redirect)
if err != nil {
return nil, err
}
return &Rewrite{rewriter: rewriter}, nil
}
//
func (rewrite *Rewrite) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
handler, err := rewrite.rewriter.NewHandler(next)
if err != nil {
log.Error("Error in rewrite middleware ", err)
return
}
handler.ServeHTTP(rw, r)
}

829
mocks/Marathon.go Normal file
View File

@@ -0,0 +1,829 @@
package mocks
import "github.com/gambol99/go-marathon"
import "github.com/stretchr/testify/mock"
import "net/url"
import "time"
// Marathon is a mock of marathon.Marathon
type Marathon struct {
mock.Mock
}
// ListApplications provides a mock function with given fields: _a0
func (_m *Marathon) ListApplications(_a0 url.Values) ([]string, error) {
ret := _m.Called(_a0)
var r0 []string
if rf, ok := ret.Get(0).(func(url.Values) []string); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(url.Values) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ApplicationVersions provides a mock function with given fields: name
func (_m *Marathon) ApplicationVersions(name string) (*marathon.ApplicationVersions, error) {
ret := _m.Called(name)
var r0 *marathon.ApplicationVersions
if rf, ok := ret.Get(0).(func(string) *marathon.ApplicationVersions); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.ApplicationVersions)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasApplicationVersion provides a mock function with given fields: name, version
func (_m *Marathon) HasApplicationVersion(name string, version string) (bool, error) {
ret := _m.Called(name, version)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
r0 = rf(name, version)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(name, version)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SetApplicationVersion provides a mock function with given fields: name, version
func (_m *Marathon) SetApplicationVersion(name string, version *marathon.ApplicationVersion) (*marathon.DeploymentID, error) {
ret := _m.Called(name, version)
var r0 *marathon.DeploymentID
if rf, ok := ret.Get(0).(func(string, *marathon.ApplicationVersion) *marathon.DeploymentID); ok {
r0 = rf(name, version)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.DeploymentID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *marathon.ApplicationVersion) error); ok {
r1 = rf(name, version)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ApplicationOK provides a mock function with given fields: name
func (_m *Marathon) ApplicationOK(name string) (bool, error) {
ret := _m.Called(name)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(name)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateApplication provides a mock function with given fields: application
func (_m *Marathon) CreateApplication(application *marathon.Application) (*marathon.Application, error) {
ret := _m.Called(application)
var r0 *marathon.Application
if rf, ok := ret.Get(0).(func(*marathon.Application) *marathon.Application); ok {
r0 = rf(application)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.Application)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*marathon.Application) error); ok {
r1 = rf(application)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteApplication provides a mock function with given fields: name
func (_m *Marathon) DeleteApplication(name string) (*marathon.DeploymentID, error) {
ret := _m.Called(name)
var r0 *marathon.DeploymentID
if rf, ok := ret.Get(0).(func(string) *marathon.DeploymentID); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.DeploymentID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateApplication provides a mock function with given fields: application
func (_m *Marathon) UpdateApplication(application *marathon.Application) (*marathon.DeploymentID, error) {
ret := _m.Called(application)
var r0 *marathon.DeploymentID
if rf, ok := ret.Get(0).(func(*marathon.Application) *marathon.DeploymentID); ok {
r0 = rf(application)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.DeploymentID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*marathon.Application) error); ok {
r1 = rf(application)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ApplicationDeployments provides a mock function with given fields: name
func (_m *Marathon) ApplicationDeployments(name string) ([]*marathon.DeploymentID, error) {
ret := _m.Called(name)
var r0 []*marathon.DeploymentID
if rf, ok := ret.Get(0).(func(string) []*marathon.DeploymentID); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*marathon.DeploymentID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// ScaleApplicationInstances provides a mock function with given fields: name, instances, force
func (_m *Marathon) ScaleApplicationInstances(name string, instances int, force bool) (*marathon.DeploymentID, error) {
ret := _m.Called(name, instances, force)
var r0 *marathon.DeploymentID
if rf, ok := ret.Get(0).(func(string, int, bool) *marathon.DeploymentID); ok {
r0 = rf(name, instances, force)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.DeploymentID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, bool) error); ok {
r1 = rf(name, instances, force)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RestartApplication provides a mock function with given fields: name, force
func (_m *Marathon) RestartApplication(name string, force bool) (*marathon.DeploymentID, error) {
ret := _m.Called(name, force)
var r0 *marathon.DeploymentID
if rf, ok := ret.Get(0).(func(string, bool) *marathon.DeploymentID); ok {
r0 = rf(name, force)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.DeploymentID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(name, force)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Applications provides a mock function with given fields: _a0
func (_m *Marathon) Applications(_a0 url.Values) (*marathon.Applications, error) {
ret := _m.Called(_a0)
var r0 *marathon.Applications
if rf, ok := ret.Get(0).(func(url.Values) *marathon.Applications); ok {
r0 = rf(_a0)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.Applications)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(url.Values) error); ok {
r1 = rf(_a0)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Application provides a mock function with given fields: name
func (_m *Marathon) Application(name string) (*marathon.Application, error) {
ret := _m.Called(name)
var r0 *marathon.Application
if rf, ok := ret.Get(0).(func(string) *marathon.Application); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.Application)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// WaitOnApplication provides a mock function with given fields: name, timeout
func (_m *Marathon) WaitOnApplication(name string, timeout time.Duration) error {
ret := _m.Called(name, timeout)
var r0 error
if rf, ok := ret.Get(0).(func(string, time.Duration) error); ok {
r0 = rf(name, timeout)
} else {
r0 = ret.Error(0)
}
return r0
}
// Tasks provides a mock function with given fields: application
func (_m *Marathon) Tasks(application string) (*marathon.Tasks, error) {
ret := _m.Called(application)
var r0 *marathon.Tasks
if rf, ok := ret.Get(0).(func(string) *marathon.Tasks); ok {
r0 = rf(application)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.Tasks)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(application)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AllTasks provides a mock function with given fields: opts
func (_m *Marathon) AllTasks(opts *marathon.AllTasksOpts) (*marathon.Tasks, error) {
ret := _m.Called(opts)
var r0 *marathon.Tasks
if rf, ok := ret.Get(0).(func(*marathon.AllTasksOpts) *marathon.Tasks); ok {
r0 = rf(opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.Tasks)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(*marathon.AllTasksOpts) error); ok {
r1 = rf(opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// TaskEndpoints provides a mock function with given fields: name, port, healthCheck
func (_m *Marathon) TaskEndpoints(name string, port int, healthCheck bool) ([]string, error) {
ret := _m.Called(name, port, healthCheck)
var r0 []string
if rf, ok := ret.Get(0).(func(string, int, bool) []string); ok {
r0 = rf(name, port, healthCheck)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]string)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, int, bool) error); ok {
r1 = rf(name, port, healthCheck)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// KillApplicationTasks provides a mock function with given fields: applicationID, opts
func (_m *Marathon) KillApplicationTasks(applicationID string, opts *marathon.KillApplicationTasksOpts) (*marathon.Tasks, error) {
ret := _m.Called(applicationID, opts)
var r0 *marathon.Tasks
if rf, ok := ret.Get(0).(func(string, *marathon.KillApplicationTasksOpts) *marathon.Tasks); ok {
r0 = rf(applicationID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.Tasks)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *marathon.KillApplicationTasksOpts) error); ok {
r1 = rf(applicationID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// KillTask provides a mock function with given fields: taskID, opts
func (_m *Marathon) KillTask(taskID string, opts *marathon.KillTaskOpts) (*marathon.Task, error) {
ret := _m.Called(taskID, opts)
var r0 *marathon.Task
if rf, ok := ret.Get(0).(func(string, *marathon.KillTaskOpts) *marathon.Task); ok {
r0 = rf(taskID, opts)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.Task)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *marathon.KillTaskOpts) error); ok {
r1 = rf(taskID, opts)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// KillTasks provides a mock function with given fields: taskIDs, opts
func (_m *Marathon) KillTasks(taskIDs []string, opts *marathon.KillTaskOpts) error {
ret := _m.Called(taskIDs, opts)
var r0 error
if rf, ok := ret.Get(0).(func([]string, *marathon.KillTaskOpts) error); ok {
r0 = rf(taskIDs, opts)
} else {
r0 = ret.Error(0)
}
return r0
}
// Groups provides a mock function with given fields:
func (_m *Marathon) Groups() (*marathon.Groups, error) {
ret := _m.Called()
var r0 *marathon.Groups
if rf, ok := ret.Get(0).(func() *marathon.Groups); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.Groups)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Group provides a mock function with given fields: name
func (_m *Marathon) Group(name string) (*marathon.Group, error) {
ret := _m.Called(name)
var r0 *marathon.Group
if rf, ok := ret.Get(0).(func(string) *marathon.Group); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.Group)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// CreateGroup provides a mock function with given fields: group
func (_m *Marathon) CreateGroup(group *marathon.Group) error {
ret := _m.Called(group)
var r0 error
if rf, ok := ret.Get(0).(func(*marathon.Group) error); ok {
r0 = rf(group)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteGroup provides a mock function with given fields: name
func (_m *Marathon) DeleteGroup(name string) (*marathon.DeploymentID, error) {
ret := _m.Called(name)
var r0 *marathon.DeploymentID
if rf, ok := ret.Get(0).(func(string) *marathon.DeploymentID); ok {
r0 = rf(name)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.DeploymentID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateGroup provides a mock function with given fields: id, group
func (_m *Marathon) UpdateGroup(id string, group *marathon.Group) (*marathon.DeploymentID, error) {
ret := _m.Called(id, group)
var r0 *marathon.DeploymentID
if rf, ok := ret.Get(0).(func(string, *marathon.Group) *marathon.DeploymentID); ok {
r0 = rf(id, group)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.DeploymentID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *marathon.Group) error); ok {
r1 = rf(id, group)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasGroup provides a mock function with given fields: name
func (_m *Marathon) HasGroup(name string) (bool, error) {
ret := _m.Called(name)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(name)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(name)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// WaitOnGroup provides a mock function with given fields: name, timeout
func (_m *Marathon) WaitOnGroup(name string, timeout time.Duration) error {
ret := _m.Called(name, timeout)
var r0 error
if rf, ok := ret.Get(0).(func(string, time.Duration) error); ok {
r0 = rf(name, timeout)
} else {
r0 = ret.Error(0)
}
return r0
}
// Deployments provides a mock function with given fields:
func (_m *Marathon) Deployments() ([]*marathon.Deployment, error) {
ret := _m.Called()
var r0 []*marathon.Deployment
if rf, ok := ret.Get(0).(func() []*marathon.Deployment); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*marathon.Deployment)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteDeployment provides a mock function with given fields: id, force
func (_m *Marathon) DeleteDeployment(id string, force bool) (*marathon.DeploymentID, error) {
ret := _m.Called(id, force)
var r0 *marathon.DeploymentID
if rf, ok := ret.Get(0).(func(string, bool) *marathon.DeploymentID); ok {
r0 = rf(id, force)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.DeploymentID)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, bool) error); ok {
r1 = rf(id, force)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// HasDeployment provides a mock function with given fields: id
func (_m *Marathon) HasDeployment(id string) (bool, error) {
ret := _m.Called(id)
var r0 bool
if rf, ok := ret.Get(0).(func(string) bool); ok {
r0 = rf(id)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string) error); ok {
r1 = rf(id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// WaitOnDeployment provides a mock function with given fields: id, timeout
func (_m *Marathon) WaitOnDeployment(id string, timeout time.Duration) error {
ret := _m.Called(id, timeout)
var r0 error
if rf, ok := ret.Get(0).(func(string, time.Duration) error); ok {
r0 = rf(id, timeout)
} else {
r0 = ret.Error(0)
}
return r0
}
// Subscriptions provides a mock function with given fields:
func (_m *Marathon) Subscriptions() (*marathon.Subscriptions, error) {
ret := _m.Called()
var r0 *marathon.Subscriptions
if rf, ok := ret.Get(0).(func() *marathon.Subscriptions); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.Subscriptions)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AddEventsListener provides a mock function with given fields: channel, filter
func (_m *Marathon) AddEventsListener(channel marathon.EventsChannel, filter int) error {
ret := _m.Called(channel, filter)
var r0 error
if rf, ok := ret.Get(0).(func(marathon.EventsChannel, int) error); ok {
r0 = rf(channel, filter)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveEventsListener provides a mock function with given fields: channel
func (_m *Marathon) RemoveEventsListener(channel marathon.EventsChannel) {
_m.Called(channel)
}
// Unsubscribe provides a mock function with given fields: _a0
func (_m *Marathon) Unsubscribe(_a0 string) error {
ret := _m.Called(_a0)
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(_a0)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetMarathonURL provides a mock function with given fields:
func (_m *Marathon) GetMarathonURL() string {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// Ping provides a mock function with given fields:
func (_m *Marathon) Ping() (bool, error) {
ret := _m.Called()
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Info provides a mock function with given fields:
func (_m *Marathon) Info() (*marathon.Info, error) {
ret := _m.Called()
var r0 *marathon.Info
if rf, ok := ret.Get(0).(func() *marathon.Info); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*marathon.Info)
}
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Leader provides a mock function with given fields:
func (_m *Marathon) Leader() (string, error) {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// AbdicateLeader provides a mock function with given fields:
func (_m *Marathon) AbdicateLeader() (string, error) {
ret := _m.Called()
var r0 string
if rf, ok := ret.Get(0).(func() string); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(string)
}
var r1 error
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}

View File

@@ -1,20 +1,20 @@
package provider
import (
"github.com/containous/traefik/types"
"github.com/docker/libkv/store"
"github.com/docker/libkv/store/boltdb"
"github.com/emilevauge/traefik/types"
)
// BoltDb holds configurations of the BoltDb provider.
type BoltDb struct {
Kv
Kv `mapstructure:",squash"`
}
// Provide allows the provider to provide configurations to traefik
// using the given configuration channel.
func (provider *BoltDb) Provide(configurationChan chan<- types.ConfigMessage) error {
provider.StoreType = store.BOLTDB
provider.storeType = store.BOLTDB
boltdb.Register()
return provider.provide(configurationChan)
}

View File

@@ -1,20 +1,20 @@
package provider
import (
"github.com/containous/traefik/types"
"github.com/docker/libkv/store"
"github.com/docker/libkv/store/consul"
"github.com/emilevauge/traefik/types"
)
// Consul holds configurations of the Consul provider.
type Consul struct {
Kv
Kv `mapstructure:",squash"`
}
// Provide allows the provider to provide configurations to traefik
// using the given configuration channel.
func (provider *Consul) Provide(configurationChan chan<- types.ConfigMessage) error {
provider.StoreType = store.CONSUL
provider.storeType = store.CONSUL
consul.Register()
return provider.provide(configurationChan)
}

199
provider/consul_catalog.go Normal file
View File

@@ -0,0 +1,199 @@
package provider
import (
"errors"
"strings"
"text/template"
"time"
log "github.com/Sirupsen/logrus"
"github.com/cenkalti/backoff"
"github.com/containous/traefik/types"
"github.com/hashicorp/consul/api"
)
const (
// DefaultWatchWaitTime is the duration to wait when polling consul
DefaultWatchWaitTime = 15 * time.Second
)
// ConsulCatalog holds configurations of the Consul catalog provider.
type ConsulCatalog struct {
BaseProvider `mapstructure:",squash"`
Endpoint string
Domain string
client *api.Client
}
type catalogUpdate struct {
Service string
Nodes []*api.ServiceEntry
}
func (provider *ConsulCatalog) watchServices(stopCh <-chan struct{}) <-chan map[string][]string {
watchCh := make(chan map[string][]string)
catalog := provider.client.Catalog()
go func() {
defer close(watchCh)
opts := &api.QueryOptions{WaitTime: DefaultWatchWaitTime}
for {
select {
case <-stopCh:
return
default:
}
data, meta, err := catalog.Services(opts)
if err != nil {
log.WithError(err).Errorf("Failed to list services")
return
}
// If LastIndex didn't change then it means `Get` returned
// because of the WaitTime and the key didn't changed.
if opts.WaitIndex == meta.LastIndex {
continue
}
opts.WaitIndex = meta.LastIndex
if data != nil {
watchCh <- data
}
}
}()
return watchCh
}
func (provider *ConsulCatalog) healthyNodes(service string) (catalogUpdate, error) {
health := provider.client.Health()
opts := &api.QueryOptions{}
data, _, err := health.Service(service, "", true, opts)
if err != nil {
log.WithError(err).Errorf("Failed to fetch details of " + service)
return catalogUpdate{}, err
}
return catalogUpdate{
Service: service,
Nodes: data,
}, nil
}
func (provider *ConsulCatalog) getBackend(node *api.ServiceEntry) string {
return strings.ToLower(node.Service.Service)
}
func (provider *ConsulCatalog) getFrontendValue(service string) string {
return service + "." + provider.Domain
}
func (provider *ConsulCatalog) buildConfig(catalog []catalogUpdate) *types.Configuration {
var FuncMap = template.FuncMap{
"getBackend": provider.getBackend,
"getFrontendValue": provider.getFrontendValue,
"replace": replace,
}
allNodes := []*api.ServiceEntry{}
serviceNames := []string{}
for _, info := range catalog {
if len(info.Nodes) > 0 {
serviceNames = append(serviceNames, info.Service)
allNodes = append(allNodes, info.Nodes...)
}
}
templateObjects := struct {
Services []string
Nodes []*api.ServiceEntry
}{
Services: serviceNames,
Nodes: allNodes,
}
configuration, err := provider.getConfiguration("templates/consul_catalog.tmpl", FuncMap, templateObjects)
if err != nil {
log.WithError(err).Error("Failed to create config")
}
return configuration
}
func (provider *ConsulCatalog) getNodes(index map[string][]string) ([]catalogUpdate, error) {
visited := make(map[string]bool)
nodes := []catalogUpdate{}
for service := range index {
name := strings.ToLower(service)
if !strings.Contains(name, " ") && !visited[name] {
visited[name] = true
log.WithFields(log.Fields{
"service": name,
}).Debug("Fetching service")
healthy, err := provider.healthyNodes(name)
if err != nil {
return nil, err
}
nodes = append(nodes, healthy)
}
}
return nodes, nil
}
func (provider *ConsulCatalog) watch(configurationChan chan<- types.ConfigMessage) error {
stopCh := make(chan struct{})
serviceCatalog := provider.watchServices(stopCh)
defer close(stopCh)
for {
select {
case index, ok := <-serviceCatalog:
if !ok {
return errors.New("Consul service list nil")
}
log.Debug("List of services changed")
nodes, err := provider.getNodes(index)
if err != nil {
return err
}
configuration := provider.buildConfig(nodes)
configurationChan <- types.ConfigMessage{
ProviderName: "consul_catalog",
Configuration: configuration,
}
}
}
}
// Provide allows the provider to provide configurations to traefik
// using the given configuration channel.
func (provider *ConsulCatalog) Provide(configurationChan chan<- types.ConfigMessage) error {
config := api.DefaultConfig()
config.Address = provider.Endpoint
client, err := api.NewClient(config)
if err != nil {
return err
}
provider.client = client
go func() {
notify := func(err error, time time.Duration) {
log.Errorf("Consul connection error %+v, retrying in %s", err, time)
}
worker := func() error {
return provider.watch(configurationChan)
}
err := backoff.RetryNotify(worker, backoff.NewExponentialBackOff(), notify)
if err != nil {
log.Fatalf("Cannot connect to consul server %+v", err)
}
}()
return err
}

View File

@@ -0,0 +1,110 @@
package provider
import (
"reflect"
"testing"
"github.com/containous/traefik/types"
"github.com/hashicorp/consul/api"
)
func TestConsulCatalogGetFrontendValue(t *testing.T) {
provider := &ConsulCatalog{
Domain: "localhost",
}
services := []struct {
service string
expected string
}{
{
service: "foo",
expected: "foo.localhost",
},
}
for _, e := range services {
actual := provider.getFrontendValue(e.service)
if actual != e.expected {
t.Fatalf("expected %q, got %q", e.expected, actual)
}
}
}
func TestConsulCatalogBuildConfig(t *testing.T) {
provider := &ConsulCatalog{
Domain: "localhost",
}
cases := []struct {
nodes []catalogUpdate
expectedFrontends map[string]*types.Frontend
expectedBackends map[string]*types.Backend
}{
{
nodes: []catalogUpdate{},
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
nodes: []catalogUpdate{
{
Service: "test",
},
},
expectedFrontends: map[string]*types.Frontend{},
expectedBackends: map[string]*types.Backend{},
},
{
nodes: []catalogUpdate{
{
Service: "test",
Nodes: []*api.ServiceEntry{
{
Service: &api.AgentService{
Service: "test",
Port: 80,
},
Node: &api.Node{
Node: "localhost",
Address: "127.0.0.1",
},
},
},
},
},
expectedFrontends: map[string]*types.Frontend{
"frontend-test": {
Backend: "backend-test",
Routes: map[string]types.Route{
"route-host-test": {
Rule: "Host",
Value: "test.localhost",
},
},
},
},
expectedBackends: map[string]*types.Backend{
"backend-test": {
Servers: map[string]types.Server{
"server-localhost-80": {
URL: "http://127.0.0.1:80",
},
},
CircuitBreaker: nil,
LoadBalancer: nil,
},
},
},
}
for _, c := range cases {
actualConfig := provider.buildConfig(c.nodes)
if !reflect.DeepEqual(actualConfig.Backends, c.expectedBackends) {
t.Fatalf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends)
}
if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) {
t.Fatalf("expected %#v, got %#v", c.expectedFrontends, actualConfig.Frontends)
}
}
}

View File

@@ -11,16 +11,16 @@ import (
"github.com/BurntSushi/ty/fun"
log "github.com/Sirupsen/logrus"
"github.com/cenkalti/backoff"
"github.com/emilevauge/traefik/types"
"github.com/containous/traefik/types"
"github.com/fsouza/go-dockerclient"
)
// Docker holds configurations of the Docker provider.
type Docker struct {
baseProvider
Endpoint string
Domain string
TLS *DockerTLS
BaseProvider `mapstructure:",squash"`
Endpoint string
Domain string
TLS *DockerTLS
}
// DockerTLS holds TLS specific configurations
@@ -107,6 +107,7 @@ func (provider *Docker) loadDockerConfig(containersInspected []docker.Container)
"getDomain": provider.getDomain,
"getProtocol": provider.getProtocol,
"getPassHostHeader": provider.getPassHostHeader,
"getEntryPoints": provider.getEntryPoints,
"getFrontendValue": provider.getFrontendValue,
"getFrontendRule": provider.getFrontendRule,
"replace": replace,
@@ -234,6 +235,13 @@ func (provider *Docker) getPassHostHeader(container docker.Container) string {
return "false"
}
func (provider *Docker) getEntryPoints(container docker.Container) []string {
if entryPoints, err := getLabel(container, "traefik.frontend.entryPoints"); err == nil {
return strings.Split(entryPoints, ",")
}
return []string{}
}
func getLabel(container docker.Container, label string) (string, error) {
for key, value := range container.Config.Labels {
if key == label {

View File

@@ -5,7 +5,7 @@ import (
"strings"
"testing"
"github.com/emilevauge/traefik/types"
"github.com/containous/traefik/types"
"github.com/fsouza/go-dockerclient"
)
@@ -682,7 +682,8 @@ func TestDockerLoadDockerConfig(t *testing.T) {
},
expectedFrontends: map[string]*types.Frontend{
`"frontend-Host-test-docker-localhost"`: {
Backend: "backend-test",
Backend: "backend-test",
EntryPoints: []string{},
Routes: map[string]types.Route{
`"route-frontend-Host-test-docker-localhost"`: {
Rule: "Host",
@@ -709,7 +710,8 @@ func TestDockerLoadDockerConfig(t *testing.T) {
Name: "test1",
Config: &docker.Config{
Labels: map[string]string{
"traefik.backend": "foobar",
"traefik.backend": "foobar",
"traefik.frontend.entryPoints": "http,https",
},
},
NetworkSettings: &docker.NetworkSettings{
@@ -736,7 +738,8 @@ func TestDockerLoadDockerConfig(t *testing.T) {
},
expectedFrontends: map[string]*types.Frontend{
`"frontend-Host-test1-docker-localhost"`: {
Backend: "backend-foobar",
Backend: "backend-foobar",
EntryPoints: []string{"http", "https"},
Routes: map[string]types.Route{
`"route-frontend-Host-test1-docker-localhost"`: {
Rule: "Host",
@@ -745,7 +748,8 @@ func TestDockerLoadDockerConfig(t *testing.T) {
},
},
`"frontend-Host-test2-docker-localhost"`: {
Backend: "backend-foobar",
Backend: "backend-foobar",
EntryPoints: []string{},
Routes: map[string]types.Route{
`"route-frontend-Host-test2-docker-localhost"`: {
Rule: "Host",

View File

@@ -1,20 +1,20 @@
package provider
import (
"github.com/containous/traefik/types"
"github.com/docker/libkv/store"
"github.com/docker/libkv/store/etcd"
"github.com/emilevauge/traefik/types"
)
// Etcd holds configurations of the Etcd provider.
type Etcd struct {
Kv
Kv `mapstructure:",squash"`
}
// Provide allows the provider to provide configurations to traefik
// using the given configuration channel.
func (provider *Etcd) Provide(configurationChan chan<- types.ConfigMessage) error {
provider.StoreType = store.ETCD
provider.storeType = store.ETCD
etcd.Register()
return provider.provide(configurationChan)
}

View File

@@ -7,13 +7,13 @@ import (
"github.com/BurntSushi/toml"
log "github.com/Sirupsen/logrus"
"github.com/emilevauge/traefik/types"
"github.com/containous/traefik/types"
"gopkg.in/fsnotify.v1"
)
// File holds configurations of the File provider.
type File struct {
baseProvider
BaseProvider `mapstructure:",squash"`
}
// Provide allows the provider to provide configurations to traefik

View File

@@ -8,23 +8,23 @@ import (
"github.com/BurntSushi/ty/fun"
log "github.com/Sirupsen/logrus"
"github.com/containous/traefik/types"
"github.com/docker/libkv"
"github.com/docker/libkv/store"
"github.com/emilevauge/traefik/types"
)
// Kv holds common configurations of key-value providers.
type Kv struct {
baseProvider
Endpoint string
Prefix string
StoreType store.Backend
kvclient store.Store
BaseProvider `mapstructure:",squash"`
Endpoint string
Prefix string
storeType store.Backend
kvclient store.Store
}
func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage) error {
kv, err := libkv.NewStore(
provider.StoreType,
provider.storeType,
[]string{provider.Endpoint},
&store.Config{
ConnectionTimeout: 30 * time.Second,
@@ -50,7 +50,7 @@ func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage) error
configuration := provider.loadConfig()
if configuration != nil {
configurationChan <- types.ConfigMessage{
ProviderName: string(provider.StoreType),
ProviderName: string(provider.storeType),
Configuration: configuration,
}
}
@@ -60,7 +60,7 @@ func (provider *Kv) provide(configurationChan chan<- types.ConfigMessage) error
}
configuration := provider.loadConfig()
configurationChan <- types.ConfigMessage{
ProviderName: string(provider.StoreType),
ProviderName: string(provider.storeType),
Configuration: configuration,
}
return nil
@@ -70,12 +70,14 @@ func (provider *Kv) loadConfig() *types.Configuration {
templateObjects := struct {
Prefix string
}{
provider.Prefix,
// Allow `/traefik/alias` to superesede `provider.Prefix`
strings.TrimSuffix(provider.get(provider.Prefix, provider.Prefix+"/alias"), "/"),
}
var KvFuncMap = template.FuncMap{
"List": provider.list,
"Get": provider.get,
"Last": provider.last,
"List": provider.list,
"Get": provider.get,
"SplitGet": provider.splitGet,
"Last": provider.last,
}
configuration, err := provider.getConfiguration("templates/kv.tmpl", KvFuncMap, templateObjects)
@@ -89,7 +91,7 @@ func (provider *Kv) list(keys ...string) []string {
joinedKeys := strings.Join(keys, "")
keysPairs, err := provider.kvclient.List(joinedKeys)
if err != nil {
log.Error("Error getting keys: ", joinedKeys, err)
log.Errorf("Error getting keys %s %s ", joinedKeys, err)
return nil
}
directoryKeys := make(map[string]string)
@@ -100,18 +102,32 @@ func (provider *Kv) list(keys ...string) []string {
return fun.Values(directoryKeys).([]string)
}
func (provider *Kv) get(keys ...string) string {
func (provider *Kv) get(defaultValue string, keys ...string) string {
joinedKeys := strings.Join(keys, "")
keyPair, err := provider.kvclient.Get(joinedKeys)
if err != nil {
log.Error("Error getting key: ", joinedKeys, err)
return ""
log.Warnf("Error getting key %s %s, setting default %s", joinedKeys, err, defaultValue)
return defaultValue
} else if keyPair == nil {
return ""
log.Warnf("Error getting key %s, setting default %s", joinedKeys, defaultValue)
return defaultValue
}
return string(keyPair.Value)
}
func (provider *Kv) splitGet(keys ...string) []string {
joinedKeys := strings.Join(keys, "")
keyPair, err := provider.kvclient.Get(joinedKeys)
if err != nil {
log.Warnf("Error getting key %s %s, setting default empty", joinedKeys, err)
return []string{}
} else if keyPair == nil {
log.Warnf("Error getting key %s, setting default %empty", joinedKeys)
return []string{}
}
return strings.Split(string(keyPair.Value), ",")
}
func (provider *Kv) last(key string) string {
splittedKey := strings.Split(key, "/")
return splittedKey[len(splittedKey)-1]

View File

@@ -176,7 +176,7 @@ func TestKvGet(t *testing.T) {
}
for _, c := range cases {
actual := c.provider.get(c.keys...)
actual := c.provider.get("", c.keys...)
if actual != c.expected {
t.Fatalf("expected %v, got %v for %v and %v", c.expected, actual, c.keys, c.provider)
}
@@ -188,7 +188,7 @@ func TestKvGet(t *testing.T) {
Error: true,
},
}
actual := provider.get("anything")
actual := provider.get("", "anything")
if actual != "" {
t.Fatalf("Should have return nil, got %v", actual)
}

View File

@@ -4,22 +4,26 @@ import (
"errors"
"net/url"
"strconv"
"strings"
"text/template"
"crypto/tls"
"github.com/BurntSushi/ty/fun"
log "github.com/Sirupsen/logrus"
"github.com/emilevauge/traefik/types"
"github.com/containous/traefik/types"
"github.com/gambol99/go-marathon"
"net/http"
)
// Marathon holds configuration of the Marathon provider.
type Marathon struct {
baseProvider
BaseProvider `mapstructure:",squash"`
Endpoint string
Domain string
NetworkInterface string
Basic *MarathonBasic
marathonClient lightMarathonClient
TLS *tls.Config
marathonClient marathon.Marathon
}
// MarathonBasic holds basic authentication specific configurations
@@ -43,6 +47,11 @@ func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage)
config.HTTPBasicAuthUser = provider.Basic.HTTPBasicAuthUser
config.HTTPBasicPassword = provider.Basic.HTTPBasicPassword
}
config.HTTPClient = &http.Client{
Transport: &http.Transport{
TLSClientConfig: provider.TLS,
},
}
client, err := marathon.NewClient(config)
if err != nil {
log.Errorf("Failed to create a client for marathon, error: %s", err)
@@ -80,14 +89,17 @@ func (provider *Marathon) Provide(configurationChan chan<- types.ConfigMessage)
func (provider *Marathon) loadMarathonConfig() *types.Configuration {
var MarathonFuncMap = template.FuncMap{
"getPort": provider.getPort,
"getWeight": provider.getWeight,
"getDomain": provider.getDomain,
"getProtocol": provider.getProtocol,
"getPassHostHeader": provider.getPassHostHeader,
"getFrontendValue": provider.getFrontendValue,
"getFrontendRule": provider.getFrontendRule,
"replace": replace,
"getBackend": provider.getBackend,
"getPort": provider.getPort,
"getWeight": provider.getWeight,
"getDomain": provider.getDomain,
"getProtocol": provider.getProtocol,
"getPassHostHeader": provider.getPassHostHeader,
"getEntryPoints": provider.getEntryPoints,
"getFrontendValue": provider.getFrontendValue,
"getFrontendRule": provider.getFrontendRule,
"getFrontendBackend": provider.getFrontendBackend,
"replace": replace,
}
applications, err := provider.marathonClient.Applications(nil)
@@ -96,7 +108,7 @@ func (provider *Marathon) loadMarathonConfig() *types.Configuration {
return nil
}
tasks, err := provider.marathonClient.AllTasks((url.Values{"status": []string{"running"}}))
tasks, err := provider.marathonClient.AllTasks(&marathon.AllTasksOpts{Status: "running"})
if err != nil {
log.Errorf("Failed to create a client for marathon, error: %s", err)
return nil
@@ -186,7 +198,7 @@ func taskFilter(task marathon.Task, applications *marathon.Applications) bool {
//filter healthchecks
if application.HasHealthChecks() {
if task.HasHealthCheckResults() {
for _, healthcheck := range task.HealthCheckResult {
for _, healthcheck := range task.HealthCheckResults {
// found one bad healthcheck, return false
if !healthcheck.Alive {
log.Debugf("Filtering marathon task %s with bad healthcheck", task.AppID)
@@ -285,6 +297,13 @@ func (provider *Marathon) getPassHostHeader(application marathon.Application) st
return "false"
}
func (provider *Marathon) getEntryPoints(application marathon.Application) []string {
if entryPoints, err := provider.getLabel(application, "traefik.frontend.entryPoints"); err == nil {
return strings.Split(entryPoints, ",")
}
return []string{}
}
// getFrontendValue returns the frontend value for the specified application, using
// it's label. It returns a default one if the label is not present.
func (provider *Marathon) getFrontendValue(application marathon.Application) string {
@@ -302,3 +321,19 @@ func (provider *Marathon) getFrontendRule(application marathon.Application) stri
}
return "Host"
}
func (provider *Marathon) getBackend(task marathon.Task, applications []marathon.Application) string {
application, errApp := getApplication(task, applications)
if errApp != nil {
log.Errorf("Unable to get marathon application from task %s", task.AppID)
return ""
}
return provider.getFrontendBackend(application)
}
func (provider *Marathon) getFrontendBackend(application marathon.Application) string {
if label, err := provider.getLabel(application, "traefik.backend"); err == nil {
return label
}
return replace("/", "-", application.ID)
}

View File

@@ -1,34 +1,32 @@
package provider
import (
"errors"
"net/url"
"reflect"
"testing"
"github.com/emilevauge/traefik/types"
"errors"
"github.com/containous/traefik/mocks"
"github.com/containous/traefik/types"
"github.com/gambol99/go-marathon"
"github.com/stretchr/testify/mock"
)
type fakeClient struct {
applicationsError bool
applications *marathon.Applications
tasksError bool
tasks *marathon.Tasks
mocks.Marathon
}
func (c *fakeClient) Applications(url.Values) (*marathon.Applications, error) {
if c.applicationsError {
return nil, errors.New("error")
func newFakeClient(applicationsError bool, applications *marathon.Applications, tasksError bool, tasks *marathon.Tasks) *fakeClient {
// create an instance of our test object
fakeClient := new(fakeClient)
if applicationsError {
fakeClient.On("Applications", mock.AnythingOfType("url.Values")).Return(nil, errors.New("error"))
}
return c.applications, nil
}
func (c *fakeClient) AllTasks(v url.Values) (*marathon.Tasks, error) {
if c.tasksError {
return nil, errors.New("error")
fakeClient.On("Applications", mock.AnythingOfType("url.Values")).Return(applications, nil)
if tasksError {
fakeClient.On("AllTasks", mock.AnythingOfType("*marathon.AllTasksOpts")).Return(nil, errors.New("error"))
}
return c.tasks, nil
fakeClient.On("AllTasks", mock.AnythingOfType("*marathon.AllTasksOpts")).Return(tasks, nil)
return fakeClient
}
func TestMarathonLoadConfig(t *testing.T) {
@@ -84,7 +82,8 @@ func TestMarathonLoadConfig(t *testing.T) {
},
expectedFrontends: map[string]*types.Frontend{
`frontend-test`: {
Backend: "backend-test",
Backend: "backend-test",
EntryPoints: []string{},
Routes: map[string]types.Route{
`route-host-test`: {
Rule: "Host",
@@ -109,14 +108,10 @@ func TestMarathonLoadConfig(t *testing.T) {
}
for _, c := range cases {
fakeClient := newFakeClient(c.applicationsError, c.applications, c.tasksError, c.tasks)
provider := &Marathon{
Domain: "docker.localhost",
marathonClient: &fakeClient{
applicationsError: c.applicationsError,
applications: c.applications,
tasksError: c.tasksError,
tasks: c.tasks,
},
Domain: "docker.localhost",
marathonClient: fakeClient,
}
actualConfig := provider.loadMarathonConfig()
if c.expectedNil {
@@ -314,7 +309,7 @@ func TestMarathonTaskFilter(t *testing.T) {
task: marathon.Task{
AppID: "foo",
Ports: []int{80},
HealthCheckResult: []*marathon.HealthCheckResult{
HealthCheckResults: []*marathon.HealthCheckResult{
{
Alive: false,
},
@@ -337,7 +332,7 @@ func TestMarathonTaskFilter(t *testing.T) {
task: marathon.Task{
AppID: "foo",
Ports: []int{80},
HealthCheckResult: []*marathon.HealthCheckResult{
HealthCheckResults: []*marathon.HealthCheckResult{
{
Alive: true,
},
@@ -378,7 +373,7 @@ func TestMarathonTaskFilter(t *testing.T) {
task: marathon.Task{
AppID: "foo",
Ports: []int{80},
HealthCheckResult: []*marathon.HealthCheckResult{
HealthCheckResults: []*marathon.HealthCheckResult{
{
Alive: true,
},
@@ -735,6 +730,36 @@ func TestMarathonGetPassHostHeader(t *testing.T) {
}
}
func TestMarathonGetEntryPoints(t *testing.T) {
provider := &Marathon{}
applications := []struct {
application marathon.Application
expected []string
}{
{
application: marathon.Application{},
expected: []string{},
},
{
application: marathon.Application{
Labels: map[string]string{
"traefik.frontend.entryPoints": "http,https",
},
},
expected: []string{"http", "https"},
},
}
for _, a := range applications {
actual := provider.getEntryPoints(a.application)
if !reflect.DeepEqual(actual, a.expected) {
t.Fatalf("expected %#v, got %#v", a.expected, actual)
}
}
}
func TestMarathonGetFrontendValue(t *testing.T) {
provider := &Marathon{
Domain: "docker.localhost",
@@ -800,3 +825,29 @@ func TestMarathonGetFrontendRule(t *testing.T) {
}
}
}
func TestMarathonGetBackend(t *testing.T) {
provider := &Marathon{}
applications := []struct {
application marathon.Application
expected string
}{
{
application: marathon.Application{
ID: "foo",
Labels: map[string]string{
"traefik.backend": "bar",
},
},
expected: "bar",
},
}
for _, a := range applications {
actual := provider.getFrontendBackend(a.application)
if actual != a.expected {
t.Fatalf("expected %q, got %q", a.expected, actual)
}
}
}

View File

@@ -7,8 +7,8 @@ import (
"text/template"
"github.com/BurntSushi/toml"
"github.com/emilevauge/traefik/autogen"
"github.com/emilevauge/traefik/types"
"github.com/containous/traefik/autogen"
"github.com/containous/traefik/types"
)
// Provider defines methods of a provider.
@@ -18,12 +18,13 @@ type Provider interface {
Provide(configurationChan chan<- types.ConfigMessage) error
}
type baseProvider struct {
// BaseProvider should be inherited by providers
type BaseProvider struct {
Watch bool
Filename string
}
func (p *baseProvider) getConfiguration(defaultTemplateFile string, funcMap template.FuncMap, templateObjects interface{}) (*types.Configuration, error) {
func (p *BaseProvider) getConfiguration(defaultTemplateFile string, funcMap template.FuncMap, templateObjects interface{}) (*types.Configuration, error) {
var (
buf []byte
err error
@@ -62,6 +63,7 @@ func replace(s1 string, s2 string, s3 string) string {
return strings.Replace(s3, s1, s2, -1)
}
// Escape beginning slash "/", convert all others to dash "-"
func getEscapedName(name string) string {
return strings.Replace(name, "/", "", -1)
return strings.Replace(strings.TrimPrefix(name, "/"), "/", "-", -1)
}

View File

@@ -9,7 +9,7 @@ import (
)
type myProvider struct {
baseProvider
BaseProvider
}
func (p *myProvider) Foo() string {
@@ -49,7 +49,7 @@ func TestConfigurationErrors(t *testing.T) {
}{
{
provider: &myProvider{
baseProvider{
BaseProvider{
Filename: "/non/existent/template.tmpl",
},
},
@@ -62,7 +62,7 @@ func TestConfigurationErrors(t *testing.T) {
},
{
provider: &myProvider{
baseProvider{
BaseProvider{
Filename: templateErrorFile.Name(),
},
},
@@ -70,7 +70,7 @@ func TestConfigurationErrors(t *testing.T) {
},
{
provider: &myProvider{
baseProvider{
BaseProvider{
Filename: templateInvalidTOMLFile.Name(),
},
},
@@ -125,7 +125,7 @@ func TestGetConfiguration(t *testing.T) {
}
provider := &myProvider{
baseProvider{
BaseProvider{
Filename: templateFile.Name(),
},
}

View File

@@ -1,9 +1,9 @@
package provider
import (
"github.com/containous/traefik/types"
"github.com/docker/libkv/store"
"github.com/docker/libkv/store/zookeeper"
"github.com/emilevauge/traefik/types"
)
// Zookepper holds configurations of the Zookepper provider.
@@ -14,7 +14,7 @@ type Zookepper struct {
// Provide allows the provider to provide configurations to traefik
// using the given configuration channel.
func (provider *Zookepper) Provide(configurationChan chan<- types.ConfigMessage) error {
provider.StoreType = store.ZK
provider.storeType = store.ZK
zookeeper.Register()
return provider.provide(configurationChan)
}

View File

@@ -3,23 +3,24 @@
if [ -z "$VALIDATE_UPSTREAM" ]; then
# this is kind of an expensive check, so let's not do this twice if we
# are running more than one validate bundlescript
VALIDATE_REPO='https://github.com/emilevauge/traefik.git'
VALIDATE_REPO='https://github.com/containous/traefik.git'
VALIDATE_BRANCH='master'
if [ "$TRAVIS" = 'true' -a "$TRAVIS_PULL_REQUEST" != 'false' ]; then
VALIDATE_REPO="https://github.com/${TRAVIS_REPO_SLUG}.git"
VALIDATE_BRANCH="${TRAVIS_BRANCH}"
fi
# Should not be needed for now O:)
# if [ "$TRAVIS" = 'true' -a "$TRAVIS_PULL_REQUEST" != 'false' ]; then
# VALIDATE_REPO="https://github.com/${TRAVIS_REPO_SLUG}.git"
# VALIDATE_BRANCH="${TRAVIS_BRANCH}"
# fi
VALIDATE_HEAD="$(git rev-parse --verify HEAD)"
git fetch -q "$VALIDATE_REPO" "refs/heads/$VALIDATE_BRANCH"
VALIDATE_UPSTREAM="$(git rev-parse --verify FETCH_HEAD)"
VALIDATE_COMMIT_LOG="$VALIDATE_UPSTREAM..$VALIDATE_HEAD"
VALIDATE_COMMIT_DIFF="$VALIDATE_UPSTREAM...$VALIDATE_HEAD"
validate_diff() {
if [ "$VALIDATE_UPSTREAM" != "$VALIDATE_HEAD" ]; then
git diff "$VALIDATE_COMMIT_DIFF" "$@"

41
script/deploy.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
set -e
if ([ "$TRAVIS_BRANCH" = "master" ] || [ ! -z "$TRAVIS_TAG" ]) && [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
echo "Deploying"
else
echo "Skipping deploy"
exit 0
fi
curl -LO https://github.com/tcnksm/ghr/releases/download/pre-release/linux_amd64.zip
unzip -q linux_amd64.zip
sudo mv ghr /usr/bin/ghr
sudo chmod +x /usr/bin/ghr
# github release and tag
ghr -t $GITHUB_TOKEN -u containous -r traefik --prerelease ${VERSION} dist/
# update traefik-library-image repo (official Docker image)
git config --global user.email "emile@vauge.com"
git config --global user.name "Emile Vauge"
git clone https://github.com/containous/traefik-library-image.git
cd traefik-library-image
git remote rm origin
git remote add origin https://emilevauge:${GITHUB_TOKEN}@github.com/containous/traefik-library-image.git
./update.sh $VERSION
git add -A
echo $VERSION | git commit --file -
echo $VERSION | git tag -a $VERSION --file -
git push --follow-tags -u origin master
# create docker image emilevauge/traefik (compatibility)
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
docker push ${REPO,,}:latest
docker tag ${REPO,,}:latest ${REPO,,}:${VERSION}
docker push ${REPO,,}:${VERSION}
cd ..
rm -Rf traefik-library-image/
echo "Deployed"

View File

@@ -1,4 +1,5 @@
#!/bin/bash
set -e
export SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
export DEST=.
@@ -6,5 +7,4 @@ export DEST=.
TESTFLAGS="$TESTFLAGS -test.timeout=30m -check.v"
cd integration
go test $TESTFLAGS
CGO_ENABLED=0 go test $TESTFLAGS

459
server.go Normal file
View File

@@ -0,0 +1,459 @@
/*
Copyright
*/
package main
import (
"crypto/tls"
"encoding/json"
"errors"
log "github.com/Sirupsen/logrus"
"github.com/codegangsta/negroni"
"github.com/containous/traefik/middlewares"
"github.com/containous/traefik/provider"
"github.com/containous/traefik/types"
"github.com/gorilla/mux"
"github.com/mailgun/manners"
"github.com/mailgun/oxy/cbreaker"
"github.com/mailgun/oxy/forward"
"github.com/mailgun/oxy/roundrobin"
"net/http"
"net/url"
"os"
"os/signal"
"reflect"
"regexp"
"sort"
"sync"
"syscall"
"time"
)
var oxyLogger = &OxyLogger{}
// Server is the reverse-proxy/load-balancer engine
type Server struct {
serverEntryPoints map[string]serverEntryPoint
configurationChan chan types.ConfigMessage
configurationValidatedChan chan types.ConfigMessage
signals chan os.Signal
stopChan chan bool
providers []provider.Provider
serverLock sync.Mutex
currentConfigurations configs
globalConfiguration GlobalConfiguration
loggerMiddleware *middlewares.Logger
}
type serverEntryPoint struct {
httpServer *manners.GracefulServer
httpRouter *mux.Router
}
// NewServer returns an initialized Server.
func NewServer(globalConfiguration GlobalConfiguration) *Server {
server := new(Server)
server.serverEntryPoints = make(map[string]serverEntryPoint)
server.configurationChan = make(chan types.ConfigMessage, 10)
server.configurationValidatedChan = make(chan types.ConfigMessage, 10)
server.signals = make(chan os.Signal, 1)
server.stopChan = make(chan bool)
server.providers = []provider.Provider{}
signal.Notify(server.signals, syscall.SIGINT, syscall.SIGTERM)
server.currentConfigurations = make(configs)
server.globalConfiguration = globalConfiguration
server.loggerMiddleware = middlewares.NewLogger(globalConfiguration.AccessLogsFile)
return server
}
// Start starts the server and blocks until server is shutted down.
func (server *Server) Start() {
go server.listenProviders()
go server.listenConfigurations()
server.configureProviders()
server.startProviders()
go server.listenSignals()
<-server.stopChan
}
// Stop stops the server
func (server *Server) Stop() {
for _, serverEntryPoint := range server.serverEntryPoints {
serverEntryPoint.httpServer.Close()
}
server.stopChan <- true
}
// Close destroys the server
func (server *Server) Close() {
close(server.configurationChan)
close(server.configurationValidatedChan)
close(server.signals)
close(server.stopChan)
server.loggerMiddleware.Close()
}
func (server *Server) listenProviders() {
lastReceivedConfiguration := time.Unix(0, 0)
lastConfigs := make(map[string]*types.ConfigMessage)
for {
configMsg := <-server.configurationChan
jsonConf, _ := json.Marshal(configMsg.Configuration)
log.Debugf("Configuration receveived from provider %s: %s", configMsg.ProviderName, string(jsonConf))
lastConfigs[configMsg.ProviderName] = &configMsg
if time.Now().After(lastReceivedConfiguration.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) {
log.Debugf("Last %s config received more than %s, OK", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration)
// last config received more than n s ago
server.configurationValidatedChan <- configMsg
} else {
log.Debugf("Last %s config received less than %s, waiting...", configMsg.ProviderName, server.globalConfiguration.ProvidersThrottleDuration)
go func() {
<-time.After(server.globalConfiguration.ProvidersThrottleDuration)
if time.Now().After(lastReceivedConfiguration.Add(time.Duration(server.globalConfiguration.ProvidersThrottleDuration))) {
log.Debugf("Waited for %s config, OK", configMsg.ProviderName)
server.configurationValidatedChan <- *lastConfigs[configMsg.ProviderName]
}
}()
}
lastReceivedConfiguration = time.Now()
}
}
func (server *Server) listenConfigurations() {
for {
configMsg := <-server.configurationValidatedChan
if configMsg.Configuration == nil {
log.Info("Skipping empty Configuration")
} else if reflect.DeepEqual(server.currentConfigurations[configMsg.ProviderName], configMsg.Configuration) {
log.Info("Skipping same configuration")
} else {
// Copy configurations to new map so we don't change current if LoadConfig fails
newConfigurations := make(configs)
for k, v := range server.currentConfigurations {
newConfigurations[k] = v
}
newConfigurations[configMsg.ProviderName] = configMsg.Configuration
newServerEntryPoints, err := server.loadConfig(newConfigurations, server.globalConfiguration)
if err == nil {
server.serverLock.Lock()
for newServerEntryPointName, newServerEntryPoint := range newServerEntryPoints {
currentServerEntryPoint := server.serverEntryPoints[newServerEntryPointName]
server.currentConfigurations = newConfigurations
currentServerEntryPoint.httpRouter = newServerEntryPoint.httpRouter
oldServer := currentServerEntryPoint.httpServer
newsrv, err := server.prepareServer(currentServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], oldServer, server.loggerMiddleware, metrics)
if err != nil {
log.Fatal("Error preparing server: ", err)
}
go server.startServer(newsrv, server.globalConfiguration)
currentServerEntryPoint.httpServer = newsrv
server.serverEntryPoints[newServerEntryPointName] = currentServerEntryPoint
time.Sleep(1 * time.Second)
if oldServer != nil {
log.Info("Stopping old server")
oldServer.Close()
}
}
server.serverLock.Unlock()
} else {
log.Error("Error loading new configuration, aborted ", err)
}
}
}
}
func (server *Server) configureProviders() {
// configure providers
if server.globalConfiguration.Docker != nil {
server.providers = append(server.providers, server.globalConfiguration.Docker)
}
if server.globalConfiguration.Marathon != nil {
server.providers = append(server.providers, server.globalConfiguration.Marathon)
}
if server.globalConfiguration.File != nil {
server.providers = append(server.providers, server.globalConfiguration.File)
}
if server.globalConfiguration.Web != nil {
server.globalConfiguration.Web.server = server
server.providers = append(server.providers, server.globalConfiguration.Web)
}
if server.globalConfiguration.Consul != nil {
server.providers = append(server.providers, server.globalConfiguration.Consul)
}
if server.globalConfiguration.ConsulCatalog != nil {
server.providers = append(server.providers, server.globalConfiguration.ConsulCatalog)
}
if server.globalConfiguration.Etcd != nil {
server.providers = append(server.providers, server.globalConfiguration.Etcd)
}
if server.globalConfiguration.Zookeeper != nil {
server.providers = append(server.providers, server.globalConfiguration.Zookeeper)
}
if server.globalConfiguration.Boltdb != nil {
server.providers = append(server.providers, server.globalConfiguration.Boltdb)
}
}
func (server *Server) startProviders() {
// start providers
for _, provider := range server.providers {
jsonConf, _ := json.Marshal(provider)
log.Infof("Starting provider %v %s", reflect.TypeOf(provider), jsonConf)
currentProvider := provider
go func() {
err := currentProvider.Provide(server.configurationChan)
if err != nil {
log.Errorf("Error starting provider %s", err)
}
}()
}
}
func (server *Server) listenSignals() {
sig := <-server.signals
log.Infof("I have to go... %+v", sig)
log.Info("Stopping server")
server.Stop()
}
// creates a TLS config that allows terminating HTTPS for multiple domains using SNI
func (server *Server) createTLSConfig(tlsOption *TLS) (*tls.Config, error) {
if tlsOption == nil {
return nil, nil
}
if len(tlsOption.Certificates) == 0 {
return nil, nil
}
config := &tls.Config{}
if config.NextProtos == nil {
config.NextProtos = []string{"http/1.1"}
}
var err error
config.Certificates = make([]tls.Certificate, len(tlsOption.Certificates))
for i, v := range tlsOption.Certificates {
config.Certificates[i], err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile)
if err != nil {
return nil, err
}
}
// BuildNameToCertificate parses the CommonName and SubjectAlternateName fields
// in each certificate and populates the config.NameToCertificate map.
config.BuildNameToCertificate()
return config, nil
}
func (server *Server) startServer(srv *manners.GracefulServer, globalConfiguration GlobalConfiguration) {
log.Info("Starting server on ", srv.Addr)
if srv.TLSConfig != nil {
err := srv.ListenAndServeTLSWithConfig(srv.TLSConfig)
if err != nil {
log.Fatal("Error creating server: ", err)
}
} else {
err := srv.ListenAndServe()
if err != nil {
log.Fatal("Error creating server: ", err)
}
}
log.Info("Server stopped")
}
func (server *Server) prepareServer(router *mux.Router, entryPoint *EntryPoint, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) {
log.Info("Preparing server")
// middlewares
var negroni = negroni.New()
for _, middleware := range middlewares {
negroni.Use(middleware)
}
negroni.UseHandler(router)
tlsConfig, err := server.createTLSConfig(entryPoint.TLS)
if err != nil {
log.Fatalf("Error creating TLS config %s", err)
return nil, err
}
if oldServer == nil {
return manners.NewWithServer(
&http.Server{
Addr: entryPoint.Address,
Handler: negroni,
TLSConfig: tlsConfig,
}), nil
}
gracefulServer, err := oldServer.HijackListener(&http.Server{
Addr: entryPoint.Address,
Handler: negroni,
TLSConfig: tlsConfig,
}, tlsConfig)
if err != nil {
log.Fatalf("Error hijacking server %s", err)
return nil, err
}
return gracefulServer, nil
}
func (server *Server) buildEntryPoints(globalConfiguration GlobalConfiguration) map[string]serverEntryPoint {
serverEntryPoints := make(map[string]serverEntryPoint)
for entryPointName := range globalConfiguration.EntryPoints {
router := server.buildDefaultHTTPRouter()
serverEntryPoints[entryPointName] = serverEntryPoint{
httpRouter: router,
}
}
return serverEntryPoints
}
// LoadConfig returns a new gorilla.mux Route from the specified global configuration and the dynamic
// provider configurations.
func (server *Server) loadConfig(configurations configs, globalConfiguration GlobalConfiguration) (map[string]serverEntryPoint, error) {
serverEntryPoints := server.buildEntryPoints(globalConfiguration)
redirectHandlers := make(map[string]http.Handler)
backends := map[string]http.Handler{}
for _, configuration := range configurations {
frontendNames := sortedFrontendNamesForConfig(configuration)
for _, frontendName := range frontendNames {
frontend := configuration.Frontends[frontendName]
log.Debugf("Creating frontend %s", frontendName)
fwd, _ := forward.New(forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader))
// default endpoints if not defined in frontends
if len(frontend.EntryPoints) == 0 {
frontend.EntryPoints = globalConfiguration.DefaultEntryPoints
}
for _, entryPointName := range frontend.EntryPoints {
log.Debugf("Wiring frontend %s to entryPoint %s", frontendName, entryPointName)
if _, ok := serverEntryPoints[entryPointName]; !ok {
return nil, errors.New("Undefined entrypoint: " + entryPointName)
}
newRoute := serverEntryPoints[entryPointName].httpRouter.NewRoute().Name(frontendName)
for routeName, route := range frontend.Routes {
log.Debugf("Creating route %s %s:%s", routeName, route.Rule, route.Value)
newRouteReflect, err := invoke(newRoute, route.Rule, route.Value)
if err != nil {
return nil, err
}
newRoute = newRouteReflect[0].Interface().(*mux.Route)
}
entryPoint := globalConfiguration.EntryPoints[entryPointName]
if entryPoint.Redirect != nil {
if redirectHandlers[entryPointName] != nil {
newRoute.Handler(redirectHandlers[entryPointName])
} else if handler, err := server.loadEntryPointConfig(entryPointName, entryPoint); err != nil {
return nil, err
} else {
newRoute.Handler(handler)
redirectHandlers[entryPointName] = handler
}
} else {
if backends[frontend.Backend] == nil {
log.Debugf("Creating backend %s", frontend.Backend)
var lb http.Handler
rr, _ := roundrobin.New(fwd)
if configuration.Backends[frontend.Backend] == nil {
return nil, errors.New("Undefined backend: " + frontend.Backend)
}
lbMethod, err := types.NewLoadBalancerMethod(configuration.Backends[frontend.Backend].LoadBalancer)
if err != nil {
configuration.Backends[frontend.Backend].LoadBalancer = &types.LoadBalancer{Method: "wrr"}
}
switch lbMethod {
case types.Drr:
log.Debugf("Creating load-balancer drr")
rebalancer, _ := roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger))
lb = rebalancer
for serverName, server := range configuration.Backends[frontend.Backend].Servers {
url, err := url.Parse(server.URL)
if err != nil {
return nil, err
}
log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight)
rebalancer.UpsertServer(url, roundrobin.Weight(server.Weight))
}
case types.Wrr:
log.Debugf("Creating load-balancer wrr")
lb = middlewares.NewWebsocketUpgrader(rr)
for serverName, server := range configuration.Backends[frontend.Backend].Servers {
url, err := url.Parse(server.URL)
if err != nil {
return nil, err
}
log.Debugf("Creating server %s at %s with weight %d", serverName, url.String(), server.Weight)
rr.UpsertServer(url, roundrobin.Weight(server.Weight))
}
}
var negroni = negroni.New()
if configuration.Backends[frontend.Backend].CircuitBreaker != nil {
log.Debugf("Creating circuit breaker %s", configuration.Backends[frontend.Backend].CircuitBreaker.Expression)
negroni.Use(middlewares.NewCircuitBreaker(lb, configuration.Backends[frontend.Backend].CircuitBreaker.Expression, cbreaker.Logger(oxyLogger)))
} else {
negroni.UseHandler(lb)
}
backends[frontend.Backend] = negroni
} else {
log.Debugf("Reusing backend %s", frontend.Backend)
}
newRoute.Handler(backends[frontend.Backend])
}
err := newRoute.GetError()
if err != nil {
log.Errorf("Error building route: %s", err)
}
}
}
}
return serverEntryPoints, nil
}
func (server *Server) loadEntryPointConfig(entryPointName string, entryPoint *EntryPoint) (http.Handler, error) {
regex := entryPoint.Redirect.Regex
replacement := entryPoint.Redirect.Replacement
if len(entryPoint.Redirect.EntryPoint) > 0 {
regex = "^(?:https?:\\/\\/)?([\\da-z\\.-]+)(?::\\d+)(.*)$"
if server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint] == nil {
return nil, errors.New("Unkown entrypoint " + entryPoint.Redirect.EntryPoint)
}
protocol := "http"
if server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].TLS != nil {
protocol = "https"
}
r, _ := regexp.Compile("(:\\d+)")
match := r.FindStringSubmatch(server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].Address)
if len(match) == 0 {
return nil, errors.New("Bad Address format: " + server.globalConfiguration.EntryPoints[entryPoint.Redirect.EntryPoint].Address)
}
replacement = protocol + "://$1" + match[0] + "$2"
}
rewrite, err := middlewares.NewRewrite(regex, replacement, true)
if err != nil {
return nil, err
}
log.Debugf("Creating entryPoint redirect %s -> %s : %s -> %s", entryPointName, entryPoint.Redirect.EntryPoint, regex, replacement)
negroni := negroni.New()
negroni.Use(rewrite)
return negroni, nil
}
func (server *Server) buildDefaultHTTPRouter() *mux.Router {
router := mux.NewRouter()
router.NotFoundHandler = http.HandlerFunc(notFoundHandler)
return router
}
func sortedFrontendNamesForConfig(configuration *types.Configuration) []string {
keys := []string{}
for key := range configuration.Frontends {
keys = append(keys, key)
}
sort.Strings(keys)
return keys
}

View File

@@ -0,0 +1,13 @@
[backends]{{range .Nodes}}
[backends.backend-{{getBackend .}}.servers.server-{{.Node.Node | replace "." "-"}}-{{.Service.Port}}]
url = "http://{{.Node.Address}}:{{.Service.Port}}"
{{end}}
[frontends]{{range .Services}}
[frontends.frontend-{{.}}]
backend = "backend-{{.}}"
passHostHeader = false
[frontends.frontend-{{.}}.routes.route-host-{{.}}]
rule = "Host"
value = "{{getFrontendValue .}}"
{{end}}

View File

@@ -8,6 +8,9 @@
[frontends."frontend-{{$frontend}}"]{{$container := index $containers 0}}
backend = "backend-{{getBackend $container}}"
passHostHeader = {{getPassHostHeader $container}}
entryPoints = [{{range getEntryPoints $container}}
"{{.}}",
{{end}}]
[frontends."frontend-{{$frontend}}".routes."route-frontend-{{$frontend}}"]
rule = "{{getFrontendRule $container}}"
value = "{{getFrontendValue $container}}"

View File

@@ -5,13 +5,13 @@
{{$backend := .}}
{{$servers := List $backend "/servers/" }}
{{$circuitBreaker := Get . "/circuitbreaker/" "expression"}}
{{$circuitBreaker := Get "" . "/circuitbreaker/" "expression"}}
{{with $circuitBreaker}}
[backends.{{Last $backend}}.circuitBreaker]
expression = "{{$circuitBreaker}}"
{{end}}
{{$loadBalancer := Get . "/loadbalancer/" "method"}}
{{$loadBalancer := Get "" . "/loadbalancer/" "method"}}
{{with $loadBalancer}}
[backends.{{Last $backend}}.loadBalancer]
method = "{{$loadBalancer}}"
@@ -19,20 +19,24 @@
{{range $servers}}
[backends.{{Last $backend}}.servers.{{Last .}}]
url = "{{Get . "/url"}}"
weight = {{Get . "/weight"}}
url = "{{Get "" . "/url"}}"
weight = {{Get "" . "/weight"}}
{{end}}
{{end}}
[frontends]{{range $frontends}}
{{$frontend := Last .}}
{{$entryPoints := SplitGet . "/entrypoints"}}
[frontends.{{$frontend}}]
backend = "{{Get . "/backend"}}"
passHostHeader = {{Get . "/passHostHeader"}}
backend = "{{Get "" . "/backend"}}"
passHostHeader = {{Get "false" . "/passHostHeader"}}
entryPoints = [{{range $entryPoints}}
"{{.}}",
{{end}}]
{{$routes := List . "/routes/"}}
{{range $routes}}
[frontends.{{$frontend}}.routes.{{Last .}}]
rule = "{{Get . "/rule"}}"
value = "{{Get . "/value"}}"
rule = "{{Get "" . "/rule"}}"
value = "{{Get "" . "/value"}}"
{{end}}
{{end}}

View File

@@ -1,14 +1,17 @@
{{$apps := .Applications}}
[backends]{{range .Tasks}}
[backends.backend{{.AppID | replace "/" "-"}}.servers.server-{{.ID | replace "." "-"}}]
[backends.backend{{getBackend . $apps}}.servers.server-{{.ID | replace "." "-"}}]
url = "{{getProtocol . $apps}}://{{.Host}}:{{getPort . $apps}}"
weight = {{getWeight . $apps}}
{{end}}
[frontends]{{range .Applications}}
[frontends.frontend{{.ID | replace "/" "-"}}]
backend = "backend{{.ID | replace "/" "-"}}"
backend = "backend{{getFrontendBackend .}}"
passHostHeader = {{getPassHostHeader .}}
entryPoints = [{{range getEntryPoints .}}
"{{.}}",
{{end}}]
[frontends.frontend{{.ID | replace "/" "-"}}.routes.route-host{{.ID | replace "/" "-"}}]
rule = "{{getFrontendRule .}}"
value = "{{getFrontendValue .}}"

View File

@@ -31,6 +31,7 @@ slave:
- /usr/bin/docker:/usr/bin/docker:ro
- /usr/lib/x86_64-linux-gnu/libapparmor.so.1:/usr/lib/x86_64-linux-gnu/libapparmor.so.1:ro
- /var/run/docker.sock:/var/run/docker.sock
- /lib/x86_64-linux-gnu/libsystemd-journal.so.0:/lib/x86_64-linux-gnu/libsystemd-journal.so.0
marathon:
image: mesosphere/marathon:v0.13.0

View File

@@ -16,10 +16,12 @@ curl -i -H "Accept: application/json" -X PUT -d "2" ht
# frontend 1
curl -i -H "Accept: application/json" -X PUT -d "backend2" http://localhost:8500/v1/kv/traefik/frontends/frontend1/backend
curl -i -H "Accept: application/json" -X PUT -d "http" http://localhost:8500/v1/kv/traefik/frontends/frontend1/entrypoints
curl -i -H "Accept: application/json" -X PUT -d "Host" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/rule
curl -i -H "Accept: application/json" -X PUT -d "test.localhost" http://localhost:8500/v1/kv/traefik/frontends/frontend1/routes/test_1/value
# frontend 2
curl -i -H "Accept: application/json" -X PUT -d "backend1" http://localhost:8500/v1/kv/traefik/frontends/frontend2/backend
curl -i -H "Accept: application/json" -X PUT -d "http,https" http://localhost:8500/v1/kv/traefik/frontends/frontend2/entrypoints
curl -i -H "Accept: application/json" -X PUT -d "Path" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/rule
curl -i -H "Accept: application/json" -X PUT -d "/test" http://localhost:8500/v1/kv/traefik/frontends/frontend2/routes/test_2/value

View File

@@ -1,385 +1,16 @@
package main
import (
"crypto/tls"
"errors"
fmtlog "log"
"net/http"
"net/url"
"os"
"os/signal"
"reflect"
"runtime"
"strings"
"syscall"
"time"
log "github.com/Sirupsen/logrus"
"github.com/codegangsta/negroni"
"github.com/emilevauge/traefik/middlewares"
"github.com/emilevauge/traefik/provider"
"github.com/emilevauge/traefik/types"
"github.com/gorilla/mux"
"github.com/mailgun/manners"
"github.com/mailgun/oxy/cbreaker"
"github.com/mailgun/oxy/forward"
"github.com/mailgun/oxy/roundrobin"
"github.com/thoas/stats"
"gopkg.in/alecthomas/kingpin.v2"
"sync"
)
var (
globalConfigFile = kingpin.Arg("conf", "Main configration file.").Default("traefik.toml").String()
version = kingpin.Flag("version", "Get Version.").Short('v').Bool()
currentConfigurations = make(configs)
metrics = stats.New()
oxyLogger = &OxyLogger{}
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
kingpin.Version(Version + " built on the " + BuildDate)
kingpin.Parse()
fmtlog.SetFlags(fmtlog.Lshortfile | fmtlog.LstdFlags)
var srv *manners.GracefulServer
var configurationRouter *mux.Router
var configurationChan = make(chan types.ConfigMessage, 10)
defer close(configurationChan)
var configurationChanValidated = make(chan types.ConfigMessage, 10)
defer close(configurationChanValidated)
var sigs = make(chan os.Signal, 1)
defer close(sigs)
var stopChan = make(chan bool)
defer close(stopChan)
var providers = []provider.Provider{}
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
var serverLock sync.Mutex
// load global configuration
globalConfiguration := LoadFileConfig(*globalConfigFile)
loggerMiddleware := middlewares.NewLogger(globalConfiguration.AccessLogsFile)
defer loggerMiddleware.Close()
// logging
level, err := log.ParseLevel(strings.ToLower(globalConfiguration.LogLevel))
if err != nil {
log.Fatal("Error getting level", err)
if err := traefikCmd.Execute(); err != nil {
fmtlog.Println(err)
os.Exit(-1)
}
log.SetLevel(level)
if len(globalConfiguration.TraefikLogsFile) > 0 {
fi, err := os.OpenFile(globalConfiguration.TraefikLogsFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
defer fi.Close()
if err != nil {
log.Fatal("Error opening file", err)
} else {
log.SetOutput(fi)
log.SetFormatter(&log.TextFormatter{DisableColors: true, FullTimestamp: true, DisableSorting: true})
}
} else {
log.SetFormatter(&log.TextFormatter{FullTimestamp: true, DisableSorting: true})
}
log.Debugf("Global configuration loaded %+v", globalConfiguration)
configurationRouter = LoadDefaultConfig(globalConfiguration)
// listen new configurations from providers
go func() {
lastReceivedConfiguration := time.Unix(0, 0)
lastConfigs := make(map[string]*types.ConfigMessage)
for {
configMsg := <-configurationChan
log.Infof("Configuration receveived from provider %s: %#v", configMsg.ProviderName, configMsg.Configuration)
lastConfigs[configMsg.ProviderName] = &configMsg
if time.Now().After(lastReceivedConfiguration.Add(time.Duration(globalConfiguration.ProvidersThrottleDuration))) {
log.Infof("Last %s config received more than %s, OK", configMsg.ProviderName, globalConfiguration.ProvidersThrottleDuration)
// last config received more than n s ago
configurationChanValidated <- configMsg
} else {
log.Infof("Last %s config received less than %s, waiting...", configMsg.ProviderName, globalConfiguration.ProvidersThrottleDuration)
go func() {
<-time.After(globalConfiguration.ProvidersThrottleDuration)
if time.Now().After(lastReceivedConfiguration.Add(time.Duration(globalConfiguration.ProvidersThrottleDuration))) {
log.Infof("Waited for %s config, OK", configMsg.ProviderName)
configurationChanValidated <- *lastConfigs[configMsg.ProviderName]
}
}()
}
lastReceivedConfiguration = time.Now()
}
}()
go func() {
for {
configMsg := <-configurationChanValidated
if configMsg.Configuration == nil {
log.Info("Skipping empty Configuration")
} else if reflect.DeepEqual(currentConfigurations[configMsg.ProviderName], configMsg.Configuration) {
log.Info("Skipping same configuration")
} else {
// Copy configurations to new map so we don't change current if LoadConfig fails
newConfigurations := make(configs)
for k, v := range currentConfigurations {
newConfigurations[k] = v
}
newConfigurations[configMsg.ProviderName] = configMsg.Configuration
newConfigurationRouter, err := LoadConfig(newConfigurations, globalConfiguration)
if err == nil {
serverLock.Lock()
currentConfigurations = newConfigurations
configurationRouter = newConfigurationRouter
oldServer := srv
newsrv, err := prepareServer(configurationRouter, globalConfiguration, oldServer, loggerMiddleware, metrics)
if err != nil {
log.Fatal("Error preparing server: ", err)
}
go startServer(newsrv, globalConfiguration)
srv = newsrv
time.Sleep(1 * time.Second)
if oldServer != nil {
log.Info("Stopping old server")
oldServer.Close()
}
serverLock.Unlock()
} else {
log.Error("Error loading new configuration, aborted ", err)
}
}
}
}()
// configure providers
if globalConfiguration.Docker != nil {
providers = append(providers, globalConfiguration.Docker)
}
if globalConfiguration.Marathon != nil {
providers = append(providers, globalConfiguration.Marathon)
}
if globalConfiguration.File != nil {
if len(globalConfiguration.File.Filename) == 0 {
// no filename, setting to global config file
globalConfiguration.File.Filename = *globalConfigFile
}
providers = append(providers, globalConfiguration.File)
}
if globalConfiguration.Web != nil {
providers = append(providers, globalConfiguration.Web)
}
if globalConfiguration.Consul != nil {
providers = append(providers, globalConfiguration.Consul)
}
if globalConfiguration.Etcd != nil {
providers = append(providers, globalConfiguration.Etcd)
}
if globalConfiguration.Zookeeper != nil {
providers = append(providers, globalConfiguration.Zookeeper)
}
if globalConfiguration.Boltdb != nil {
providers = append(providers, globalConfiguration.Boltdb)
}
// start providers
for _, provider := range providers {
log.Infof("Starting provider %v %+v", reflect.TypeOf(provider), provider)
currentProvider := provider
go func() {
err := currentProvider.Provide(configurationChan)
if err != nil {
log.Errorf("Error starting provider %s", err)
}
}()
}
go func() {
sig := <-sigs
log.Infof("I have to go... %+v", sig)
log.Info("Stopping server")
srv.Close()
stopChan <- true
}()
//negroni.Use(middlewares.NewCircuitBreaker(oxyLogger))
//negroni.Use(middlewares.NewRoutes(configurationRouter))
var er error
serverLock.Lock()
srv, er = prepareServer(configurationRouter, globalConfiguration, nil, loggerMiddleware, metrics)
if er != nil {
log.Fatal("Error preparing server: ", er)
}
go startServer(srv, globalConfiguration)
//TODO change that!
time.Sleep(100 * time.Millisecond)
serverLock.Unlock()
<-stopChan
log.Info("Shutting down")
}
// creates a TLS config that allows terminating HTTPS for multiple domains using SNI
func createTLSConfig(certs []Certificate) (*tls.Config, error) {
if len(certs) == 0 {
return nil, nil
}
config := &tls.Config{}
if config.NextProtos == nil {
config.NextProtos = []string{"http/1.1"}
}
var err error
config.Certificates = make([]tls.Certificate, len(certs))
for i, v := range certs {
config.Certificates[i], err = tls.LoadX509KeyPair(v.CertFile, v.KeyFile)
if err != nil {
return nil, err
}
}
// BuildNameToCertificate parses the CommonName and SubjectAlternateName fields
// in each certificate and populates the config.NameToCertificate map.
config.BuildNameToCertificate()
return config, nil
}
func startServer(srv *manners.GracefulServer, globalConfiguration *GlobalConfiguration) {
log.Info("Starting server")
if srv.TLSConfig != nil {
err := srv.ListenAndServeTLSWithConfig(srv.TLSConfig)
if err != nil {
log.Fatal("Error creating server: ", err)
}
} else {
err := srv.ListenAndServe()
if err != nil {
log.Fatal("Error creating server: ", err)
}
}
log.Info("Server stopped")
}
func prepareServer(router *mux.Router, globalConfiguration *GlobalConfiguration, oldServer *manners.GracefulServer, middlewares ...negroni.Handler) (*manners.GracefulServer, error) {
log.Info("Preparing server")
// middlewares
var negroni = negroni.New()
for _, middleware := range middlewares {
negroni.Use(middleware)
}
negroni.UseHandler(router)
tlsConfig, err := createTLSConfig(globalConfiguration.Certificates)
if err != nil {
log.Fatalf("Error creating TLS config %s", err)
return nil, err
}
if oldServer == nil {
return manners.NewWithServer(
&http.Server{
Addr: globalConfiguration.Port,
Handler: negroni,
TLSConfig: tlsConfig,
}), nil
}
server, err := oldServer.HijackListener(&http.Server{
Addr: globalConfiguration.Port,
Handler: negroni,
TLSConfig: tlsConfig,
}, tlsConfig)
if err != nil {
log.Fatalf("Error hijacking server %s", err)
return nil, err
}
return server, nil
}
// LoadConfig returns a new gorrilla.mux Route from the specified global configuration and the dynamic
// provider configurations.
func LoadConfig(configurations configs, globalConfiguration *GlobalConfiguration) (*mux.Router, error) {
router := mux.NewRouter()
router.NotFoundHandler = http.HandlerFunc(notFoundHandler)
backends := map[string]http.Handler{}
for _, configuration := range configurations {
for frontendName, frontend := range configuration.Frontends {
log.Debugf("Creating frontend %s", frontendName)
fwd, _ := forward.New(forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader))
newRoute := router.NewRoute().Name(frontendName)
for routeName, route := range frontend.Routes {
log.Debugf("Creating route %s %s:%s", routeName, route.Rule, route.Value)
newRouteReflect, err := invoke(newRoute, route.Rule, route.Value)
if err != nil {
return nil, err
}
newRoute = newRouteReflect[0].Interface().(*mux.Route)
}
if backends[frontend.Backend] == nil {
log.Debugf("Creating backend %s", frontend.Backend)
var lb http.Handler
rr, _ := roundrobin.New(fwd)
if configuration.Backends[frontend.Backend] == nil {
return nil, errors.New("Backend not found: " + frontend.Backend)
}
lbMethod, err := types.NewLoadBalancerMethod(configuration.Backends[frontend.Backend].LoadBalancer)
if err != nil {
configuration.Backends[frontend.Backend].LoadBalancer = &types.LoadBalancer{Method: "wrr"}
}
switch lbMethod {
case types.Drr:
log.Infof("Creating load-balancer drr")
rebalancer, _ := roundrobin.NewRebalancer(rr, roundrobin.RebalancerLogger(oxyLogger))
lb = rebalancer
for serverName, server := range configuration.Backends[frontend.Backend].Servers {
url, err := url.Parse(server.URL)
if err != nil {
return nil, err
}
log.Infof("Creating server %s %s", serverName, url.String())
rebalancer.UpsertServer(url, roundrobin.Weight(server.Weight))
}
case types.Wrr:
log.Infof("Creating load-balancer wrr")
lb = middlewares.NewWebsocketUpgrader(rr)
for serverName, server := range configuration.Backends[frontend.Backend].Servers {
url, err := url.Parse(server.URL)
if err != nil {
return nil, err
}
log.Infof("Creating server %s %s", serverName, url.String())
rr.UpsertServer(url, roundrobin.Weight(server.Weight))
}
}
var negroni = negroni.New()
if configuration.Backends[frontend.Backend].CircuitBreaker != nil {
log.Infof("Creating circuit breaker %s", configuration.Backends[frontend.Backend].CircuitBreaker.Expression)
negroni.Use(middlewares.NewCircuitBreaker(lb, configuration.Backends[frontend.Backend].CircuitBreaker.Expression, cbreaker.Logger(oxyLogger)))
} else {
negroni.UseHandler(lb)
}
backends[frontend.Backend] = negroni
} else {
log.Infof("Reusing backend %s", frontend.Backend)
}
// stream.New(backends[frontend.Backend], stream.Retry("IsNetworkError() && Attempts() <= " + strconv.Itoa(globalConfiguration.Replay)), stream.Logger(oxyLogger))
newRoute.Handler(backends[frontend.Backend])
err := newRoute.GetError()
if err != nil {
log.Errorf("Error building route: %s", err)
}
}
}
return router, nil
}
// Invoke calls the specified method with the specified arguments on the specified interface.
// It uses the go(lang) reflect package.
func invoke(any interface{}, name string, args ...interface{}) ([]reflect.Value, error) {
inputs := make([]reflect.Value, len(args))
for i := range args {
inputs[i] = reflect.ValueOf(args[i])
}
method := reflect.ValueOf(any).MethodByName(name)
if method.IsValid() {
return method.Call(inputs), nil
}
return nil, errors.New("Method not found: " + name)
os.Exit(0)
}

View File

@@ -2,12 +2,45 @@
# Global configuration
################################################################
# Reverse proxy port
# Entrypoints definition
#
# Optional
# Default: ":80"
# Default:
# [entryPoints]
# [entryPoints.http]
# address = ":80"
#
# port = ":80"
# To redirect an http entrypoint to an https entrypoint (with SNI support):
# [entryPoints]
# [entryPoints.http]
# address = ":80"
# [entryPoints.http.redirect]
# entryPoint = "https"
# [entryPoints.https]
# address = ":443"
# [entryPoints.https.tls]
# [[entryPoints.https.tls.certificates]]
# CertFile = "integration/fixtures/https/snitest.com.cert"
# KeyFile = "integration/fixtures/https/snitest.com.key"
# [[entryPoints.https.tls.certificates]]
# CertFile = "integration/fixtures/https/snitest.org.cert"
# KeyFile = "integration/fixtures/https/snitest.org.key"
#
# To redirect an entrypoint rewriting the URL:
# [entryPoints]
# [entryPoints.http]
# address = ":80"
# [entryPoints.http.redirect]
# regex = "^http://localhost/(.*)"
# replacement = "http://mydomain/$1"
# Entrypoints to be used by frontends that do not specify any entrypoint.
# Each frontend can specify its own entrypoints.
#
# Optional
# Default: ["http"]
#
# defaultEntryPoints = ["http", "https"]
# Timeout in seconds.
# Duration to give active requests a chance to finish during hot-reloads
@@ -37,15 +70,6 @@
#
# logLevel = "ERROR"
# SSL certificates and keys
# You may add several certificate/key pairs to terminate HTTPS for multiple domain names using TLS SNI
#
# Optional
#
# [[certificates]]
# CertFile = "traefik.crt"
# KeyFile = "traefik.key"
# Backends throttle duration: minimum duration between 2 events from providers
# before applying a new configuration. It avoids unnecessary reloads if multiples events
# are sent in a short amount of time.
@@ -55,6 +79,14 @@
#
# ProvidersThrottleDuration = "5s"
# If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used.
# If you encounter 'too many open files' errors, you can either change this value, or change `ulimit` value.
#
# Optional
# Default: http.DefaultMaxIdleConnsPerHost
#
# MaxIdleConnsPerHost = 200
################################################################
# Web configuration backend
@@ -78,6 +110,11 @@
#
# CertFile = "traefik.crt"
# KeyFile = "traefik.key"
#
# Set REST API to read-only mode
#
# Optional
# ReadOnly = false
################################################################
@@ -202,6 +239,13 @@
# httpBasicAuthUser = "foo"
# httpBasicPassword = "bar"
# TLS client configuration. https://golang.org/pkg/crypto/tls/#Config
#
# Optional
#
# [marathon.TLS]
# InsecureSkipVerify = true
################################################################
# Consul KV configuration backend
@@ -377,6 +421,12 @@
# [frontends.frontend2]
# backend = "backend1"
# passHostHeader = true
# [frontends.frontend2.routes.test_2]
# entrypoints = ["https"] # overrides defaultEntryPoints
# [frontends.frontend2.routes.test_1]
# rule = "Host"
# value = "{subdomain:[a-z]+}.localhost"
# [frontends.frontend3]
# entrypoints = ["http", "https"] # overrides defaultEntryPoints
# backend = "backend2"
# rule = "Path"
# value = "/test"

View File

@@ -36,6 +36,7 @@ type Route struct {
// Frontend holds frontend configuration.
type Frontend struct {
EntryPoints []string `json:"entryPoints,omitempty"`
Backend string `json:"backend,omitempty"`
Routes map[string]Route `json:"routes,omitempty"`
PassHostHeader bool `json:"passHostHeader,omitempty"`

23
utils.go Normal file
View File

@@ -0,0 +1,23 @@
/*
Copyright
*/
package main
import (
"errors"
"reflect"
)
// Invoke calls the specified method with the specified arguments on the specified interface.
// It uses the go(lang) reflect package.
func invoke(any interface{}, name string, args ...interface{}) ([]reflect.Value, error) {
inputs := make([]reflect.Value, len(args))
for i := range args {
inputs[i] = reflect.ValueOf(args[i])
}
method := reflect.ValueOf(any).MethodByName(name)
if method.IsValid() {
return method.Call(inputs), nil
}
return nil, errors.New("Method not found: " + name)
}

76
web.go
View File

@@ -7,19 +7,23 @@ import (
"net/http"
log "github.com/Sirupsen/logrus"
"github.com/containous/traefik/autogen"
"github.com/containous/traefik/types"
"github.com/elazarl/go-bindata-assetfs"
"github.com/emilevauge/traefik/autogen"
"github.com/emilevauge/traefik/types"
"github.com/gorilla/mux"
"github.com/thoas/stats"
"github.com/unrolled/render"
)
var metrics = stats.New()
// WebProvider is a provider.Provider implementation that provides the UI.
// FIXME to be handled another way.
type WebProvider struct {
Address string
CertFile, KeyFile string
ReadOnly bool
server *Server
}
var (
@@ -34,12 +38,12 @@ func (provider *WebProvider) Provide(configurationChan chan<- types.ConfigMessag
systemRouter := mux.NewRouter()
// health route
systemRouter.Methods("GET").Path("/health").HandlerFunc(getHealthHandler)
systemRouter.Methods("GET").Path("/health").HandlerFunc(provider.getHealthHandler)
// API routes
systemRouter.Methods("GET").Path("/api").HandlerFunc(getConfigHandler)
systemRouter.Methods("GET").Path("/api/providers").HandlerFunc(getConfigHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}").HandlerFunc(getProviderHandler)
systemRouter.Methods("GET").Path("/api").HandlerFunc(provider.getConfigHandler)
systemRouter.Methods("GET").Path("/api/providers").HandlerFunc(provider.getConfigHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}").HandlerFunc(provider.getProviderHandler)
systemRouter.Methods("PUT").Path("/api/providers/{provider}").HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if provider.ReadOnly {
response.WriteHeader(http.StatusForbidden)
@@ -58,20 +62,20 @@ func (provider *WebProvider) Provide(configurationChan chan<- types.ConfigMessag
err := json.Unmarshal(body, configuration)
if err == nil {
configurationChan <- types.ConfigMessage{"web", configuration}
getConfigHandler(response, request)
provider.getConfigHandler(response, request)
} else {
log.Errorf("Error parsing configuration %+v", err)
http.Error(response, fmt.Sprintf("%+v", err), http.StatusBadRequest)
}
})
systemRouter.Methods("GET").Path("/api/providers/{provider}/backends").HandlerFunc(getBackendsHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/backends/{backend}").HandlerFunc(getBackendHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/backends/{backend}/servers").HandlerFunc(getServersHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/backends/{backend}/servers/{server}").HandlerFunc(getServerHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/frontends").HandlerFunc(getFrontendsHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/frontends/{frontend}").HandlerFunc(getFrontendHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/frontends/{frontend}/routes").HandlerFunc(getRoutesHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/frontends/{frontend}/routes/{route}").HandlerFunc(getRouteHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/backends").HandlerFunc(provider.getBackendsHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/backends/{backend}").HandlerFunc(provider.getBackendHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/backends/{backend}/servers").HandlerFunc(provider.getServersHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/backends/{backend}/servers/{server}").HandlerFunc(provider.getServerHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/frontends").HandlerFunc(provider.getFrontendsHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/frontends/{frontend}").HandlerFunc(provider.getFrontendHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/frontends/{frontend}/routes").HandlerFunc(provider.getRoutesHandler)
systemRouter.Methods("GET").Path("/api/providers/{provider}/frontends/{frontend}/routes/{route}").HandlerFunc(provider.getRouteHandler)
// Expose dashboard
systemRouter.Methods("GET").Path("/").HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
@@ -95,39 +99,39 @@ func (provider *WebProvider) Provide(configurationChan chan<- types.ConfigMessag
return nil
}
func getHealthHandler(response http.ResponseWriter, request *http.Request) {
func (provider *WebProvider) getHealthHandler(response http.ResponseWriter, request *http.Request) {
templatesRenderer.JSON(response, http.StatusOK, metrics.Data())
}
func getConfigHandler(response http.ResponseWriter, request *http.Request) {
templatesRenderer.JSON(response, http.StatusOK, currentConfigurations)
func (provider *WebProvider) getConfigHandler(response http.ResponseWriter, request *http.Request) {
templatesRenderer.JSON(response, http.StatusOK, provider.server.currentConfigurations)
}
func getProviderHandler(response http.ResponseWriter, request *http.Request) {
func (provider *WebProvider) getProviderHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
if provider, ok := currentConfigurations[providerID]; ok {
if provider, ok := provider.server.currentConfigurations[providerID]; ok {
templatesRenderer.JSON(response, http.StatusOK, provider)
} else {
http.NotFound(response, request)
}
}
func getBackendsHandler(response http.ResponseWriter, request *http.Request) {
func (provider *WebProvider) getBackendsHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
if provider, ok := currentConfigurations[providerID]; ok {
if provider, ok := provider.server.currentConfigurations[providerID]; ok {
templatesRenderer.JSON(response, http.StatusOK, provider.Backends)
} else {
http.NotFound(response, request)
}
}
func getBackendHandler(response http.ResponseWriter, request *http.Request) {
func (provider *WebProvider) getBackendHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
backendID := vars["backend"]
if provider, ok := currentConfigurations[providerID]; ok {
if provider, ok := provider.server.currentConfigurations[providerID]; ok {
if backend, ok := provider.Backends[backendID]; ok {
templatesRenderer.JSON(response, http.StatusOK, backend)
return
@@ -136,11 +140,11 @@ func getBackendHandler(response http.ResponseWriter, request *http.Request) {
http.NotFound(response, request)
}
func getServersHandler(response http.ResponseWriter, request *http.Request) {
func (provider *WebProvider) getServersHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
backendID := vars["backend"]
if provider, ok := currentConfigurations[providerID]; ok {
if provider, ok := provider.server.currentConfigurations[providerID]; ok {
if backend, ok := provider.Backends[backendID]; ok {
templatesRenderer.JSON(response, http.StatusOK, backend.Servers)
return
@@ -149,12 +153,12 @@ func getServersHandler(response http.ResponseWriter, request *http.Request) {
http.NotFound(response, request)
}
func getServerHandler(response http.ResponseWriter, request *http.Request) {
func (provider *WebProvider) getServerHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
backendID := vars["backend"]
serverID := vars["server"]
if provider, ok := currentConfigurations[providerID]; ok {
if provider, ok := provider.server.currentConfigurations[providerID]; ok {
if backend, ok := provider.Backends[backendID]; ok {
if server, ok := backend.Servers[serverID]; ok {
templatesRenderer.JSON(response, http.StatusOK, server)
@@ -165,21 +169,21 @@ func getServerHandler(response http.ResponseWriter, request *http.Request) {
http.NotFound(response, request)
}
func getFrontendsHandler(response http.ResponseWriter, request *http.Request) {
func (provider *WebProvider) getFrontendsHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
if provider, ok := currentConfigurations[providerID]; ok {
if provider, ok := provider.server.currentConfigurations[providerID]; ok {
templatesRenderer.JSON(response, http.StatusOK, provider.Frontends)
} else {
http.NotFound(response, request)
}
}
func getFrontendHandler(response http.ResponseWriter, request *http.Request) {
func (provider *WebProvider) getFrontendHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
frontendID := vars["frontend"]
if provider, ok := currentConfigurations[providerID]; ok {
if provider, ok := provider.server.currentConfigurations[providerID]; ok {
if frontend, ok := provider.Frontends[frontendID]; ok {
templatesRenderer.JSON(response, http.StatusOK, frontend)
return
@@ -188,11 +192,11 @@ func getFrontendHandler(response http.ResponseWriter, request *http.Request) {
http.NotFound(response, request)
}
func getRoutesHandler(response http.ResponseWriter, request *http.Request) {
func (provider *WebProvider) getRoutesHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
frontendID := vars["frontend"]
if provider, ok := currentConfigurations[providerID]; ok {
if provider, ok := provider.server.currentConfigurations[providerID]; ok {
if frontend, ok := provider.Frontends[frontendID]; ok {
templatesRenderer.JSON(response, http.StatusOK, frontend.Routes)
return
@@ -201,12 +205,12 @@ func getRoutesHandler(response http.ResponseWriter, request *http.Request) {
http.NotFound(response, request)
}
func getRouteHandler(response http.ResponseWriter, request *http.Request) {
func (provider *WebProvider) getRouteHandler(response http.ResponseWriter, request *http.Request) {
vars := mux.Vars(request)
providerID := vars["provider"]
frontendID := vars["frontend"]
routeID := vars["route"]
if provider, ok := currentConfigurations[providerID]; ok {
if provider, ok := provider.server.currentConfigurations[providerID]; ok {
if frontend, ok := provider.Frontends[frontendID]; ok {
if route, ok := frontend.Routes[routeID]; ok {
templatesRenderer.JSON(response, http.StatusOK, route)

View File

@@ -11,8 +11,8 @@ COPY bower.json $WEBUI_DIR/
WORKDIR $WEBUI_DIR
RUN npm set progress=false
RUN npm install
RUN bower install --allow-root
RUN npm install --quiet
RUN bower install --allow-root --quiet
COPY . $WEBUI_DIR/

View File

@@ -17,6 +17,7 @@
</table>
</div>
<div data-bg-show="frontendCtrl.frontend.backend" class="panel-footer">
<span data-ng-repeat="entryPoint in frontendCtrl.frontend.entryPoints"><span class="label label-primary">{{entryPoint}}</span><span data-ng-hide="$last">&nbsp;</span></span>
<span class="label label-warning" role="button" data-toggle="collapse" href="#{{frontendCtrl.frontend.backend}}" aria-expanded="false">{{frontendCtrl.frontend.backend}}</span>
<span data-ng-show="frontendCtrl.frontend.passHostHeader" class="label label-warning">Pass Host Header</span>
</div>

View File

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