1
0
mirror of https://github.com/containous/traefik.git synced 2025-09-30 17:44:25 +03:00

Compare commits

..

72 Commits

Author SHA1 Message Date
Ludovic Fernandez
78544f7fa2 Prepare release v1.3.6 2017-08-22 09:52:02 +02:00
Ludovic Fernandez
40e18db838 Websocket parameters and protocol. 2017-08-20 19:02:02 +02:00
Ludovic Fernandez
413ed62933 Prepare release v1.3.5 2017-08-01 17:43:37 +02:00
SALLEYRON Julien
1b4dc3783c Oxy with fixes on websocket + integration tests 2017-08-01 15:24:08 +02:00
Julien Salleyron
1db9482a8e Prepare release v1.3.4 2017-07-27 17:24:19 +02:00
Julien Salleyron
888e6dcbc8 Oxy with gorilla for websocket(+integration tests) 2017-07-27 15:43:12 +02:00
dedalusj
a09a8b1235 Fix replace path rule
* Fix replace path rule
* test: add RequestURI tests.
2017-07-19 10:27:52 +02:00
Fernandez Ludovic
36ee69609e fix: double compression. 2017-07-18 11:27:24 +02:00
Fernandez Ludovic
98b52d1f54 Prepare release v1.3.3 2017-07-06 17:53:35 +02:00
Timo Reimann
4892b2b0da [kubernetes] Undo the Secrets controller sync wait.
When Secrets permissions have not been granted (which is likely to be
the case for users not needing the basic auth feature), the watch on the
Secrets API will never yield a response, thereby causing the controller
to never sync successfully, and in turn causing the check for all
controller synchronizations to fail consistently. Thus, no event will
ever be handled.
2017-07-06 17:12:25 +02:00
Timo Reimann
91ce78da46 [k8s] Tell glog to log everything into STDERR.
Logging errors into a file inside a minimalistic container might not be
possible, and glog bails out with an exit code > 0 if it fails.
2017-07-04 17:11:50 +02:00
Fernandez Ludovic
f06e256934 Prepare release v1.3.2 2017-06-29 17:40:11 +02:00
Fernandez Ludovic
4699d6be18 Fix proxying of unannounced trailers 2017-06-29 17:03:29 +02:00
Timo Reimann
6473002021 Continue Ingress processing on auth retrieval failure. 2017-06-29 16:13:53 +02:00
Timo Reimann
4d89ff7e18 Improve basic auth handling.
- Enrich logging.
- Move error closer to producer.
2017-06-29 16:13:53 +02:00
Timo Reimann
c5c63071ca Wait for secret controller to finish synchronizing.
Prevents a race on closing the events channel, possibly leading to a
double-close.
2017-06-29 16:13:53 +02:00
Timo Reimann
9fbe21c534 Upgrade go-marathon to dd6cbd4.
Fixes a problem with UnreachableStrategy being available now in two
type-incompatible formats (object and string).

We also upgrade the transitive dependency
github.com/donovanhide/eventsource.
2017-06-29 09:59:20 +02:00
Fernandez Ludovic
7a34303593 chore: Bump Docker version to 17.03 2017-06-27 23:22:43 +02:00
Fernandez Ludovic
fdb24c64e4 chore(semaphoreci): update Docker version. 2017-06-27 14:05:44 +02:00
nmengin
631079a12f feature: Add provided certificates check before to generate ACME certificate when OnHostRule is activated
- ADD TI to check the new behaviour with onHostRule and provided certificates
- ADD TU on the getProvidedCertificate method
2017-06-26 18:32:55 +02:00
Fernandez Ludovic
f99f3b987e fix: websocket when the connection upgrade failed. 2017-06-26 18:00:03 +02:00
Fernandez Ludovic
fe4d0e95b3 Prepare release v1.3.1 2017-06-16 12:53:26 +02:00
Fernandez Ludovic
0fb63f4488 fix(webui): don't fail when backend or frontend are empty. 2017-06-16 10:38:58 +02:00
Fernandez Ludovic
d87c4d89e9 fix: Double GZIP. 2017-06-14 21:13:38 +02:00
Fernandez Ludovic
ccc429e36c refactor(eureka): Use Traefik Logger. 2017-06-14 19:49:45 +02:00
Fernandez Ludovic
0d25ba3cbc refactor: Add explicit error message. 2017-06-14 19:49:45 +02:00
Kekoa Vincent
ac5ab13a4c Fix errors caused by incorrect type being sent for the Kubernetes Secret watcher #1596
This was likely just a copy-paste issue, the bug should be benign because the secret is cast to the correct type later, but the additional logging is a major annoyance, and is happening even if basic auth is not in use with Kubernetes.
2017-06-02 19:20:47 +02:00
Maxime Guyot
1db22a6e63 Fix capitalization of PathPrefixStrip in kubernetes doc 2017-06-01 20:40:28 +02:00
Fernandez Ludovic
e1e07f7750 Prepare release v1.3.0 2017-05-31 10:11:16 -07:00
Fernandez Ludovic
4c4eba4b56 doc(changelog): replace GitHub API URL by HTML URL. 2017-05-30 19:48:01 +02:00
Fernandez Ludovic
dbfd2663c2 Prepare release v1.3.0-rc3 2017-05-24 15:32:29 +02:00
Fernandez Ludovic
5b896bb46c fix: Empty Rancher launch config. 2017-05-24 11:20:30 +02:00
Josh Toft
bc0121808a Fix behavior for PathPrefixStrip
When pushing data to downstream proxies; malformed requests were being
sent.

The corrected behavior is as follows:

| Route Stripped    |     URL                |  Passed to Backend |
| ----------------- | ---------------------- | ------------------ |
| /                 |     /                  |  /                 |

| Route Stripped    |     URL                |  Passed to Backend |
| ----------------- | ---------------------- | ------------------ |
| /stat             |     /stat              |  /                 |
| /stat             |     /stat/             |  /                 |
| /stat             |     /status            |  /status           |
| /stat             |     /stat/us           |  /us               |

| Route Stripped    |     URL                |  Passed to Backend |
| ----------------- | ---------------------- | ------------------ |
| /stat/            |     /stat              |  /stat             |
| /stat/            |     /stat/             |  /                 |
| /stat/            |     /status            |  /status           |
| /stat/            |     /stat/us           |  /us               |

Prior, we could strip the prefixing `/`, and we'd also ignore the case
where you want to serve something like `/api` as both the index and as a
subpath.

Additionally, this should resolve a myriad of issues relating to
kubernetes ingress `PathPrefixStrip`.
2017-05-24 10:50:12 +02:00
Timo Reimann
4293446111 Install github.com/stretchr/testify/require. 2017-05-24 00:51:48 +02:00
Timo Reimann
9967494996 [k8s] Ignore Ingresses with empty Endpoint subsets.
We previously fell back to using ClusterIPs. However, the approach can
lead to all kinds of problems since Ingresses rely on being able to talk
to Endpoints directly. For instance, it can break stickiness and
retries.
2017-05-23 21:15:06 +02:00
Timo Reimann
b392023c37 Add additional tests for PathStrip{Prefix}. 2017-05-23 17:31:34 +02:00
Timo Reimann
f7d9dfafd0 [k8s] Remove rule type path list.
Instead of doing sanity checks in the Kubernetes provider, we just
accept any non-empty value from the annotation and rely on the server
part to filter out unknown rules.

This allows us to automatically stay in sync with the currently
supported Path matchers/modifiers.
2017-05-23 17:31:34 +02:00
Timo Reimann
219a6372b0 Upgrade go-marathon to 15ea23e.
Our vendored copy contains a bug that causes unavailable Marathon nodes
to never be marked as available again due to a misconstruction in the
URL to the Marathon health check / ping endpoint used by go-marathon
internally.

A fix[1] has been published.

[1]https://github.com/gambol99/go-marathon/pull/283
2017-05-22 20:52:24 +02:00
Fernandez Ludovic
2e762e76f3 doc: update change log. 2017-05-22 10:26:05 +02:00
tanyadegurechaff
987ae92f53 Create log folder if not present 2017-05-19 15:49:02 +02:00
Ed Robinson
c1220b8765 Re Orginise k8s docs to make 1.6 usage easier
* Adds some raw.githubusercontent.com links to the kubectl examples to
make following along at home simpler.
* Dedupe the config for rbac so it can just be ommited if not needed.
2017-05-17 15:58:54 +02:00
Emile Vauge
bc6f764a87 Merge pull request #1578 from Stibbons/marathon_doc
Add Marathon guide.
2017-05-17 15:21:09 +02:00
Gaetan Semet
0b414ed482 Add Marathon guide
Copy/pasted from very comprehensive slack response from @ttr
https://traefik.slack.com/archives/C0CDT22PJ/p1494347929571784?thread_ts=1494339388.375916&cid=C0CDT22PJ

Signed-off-by: Gaetan Semet <gaetan@xeberon.net>
2017-05-17 14:59:28 +02:00
Emile Vauge
f521e72f15 Merge pull request #1612 from containous/fix-deploy
Fix deploy script, removes Docker version check
2017-05-16 17:53:47 +02:00
Emile Vauge
88ea0a037b Fix deploy script, removes Docker version check 2017-05-16 17:24:08 +02:00
Emile Vauge
c963cee3c8 Merge pull request #1606 from containous/prepare-release-v1.3.0-rc2
Prepare release v1.3.0-rc2
2017-05-16 16:15:05 +02:00
Emile Vauge
0be353d435 Merge pull request #1610 from ldez/beta-cluster
doc: Traefik cluster in beta.
2017-05-16 15:50:03 +02:00
Emile Vauge
6afff2d403 Merge pull request #1610 from ldez/beta-cluster
doc: Traefik cluster in beta.
2017-05-16 15:47:11 +02:00
Fernandez Ludovic
12fa144f2f doc: Traefik cluster in beta. 2017-05-16 15:28:18 +02:00
Emile Vauge
ac0e48b48c Merge pull request #1608 from ldez/feat-semaphoreci
SemaphoreCI on 1.3 branch
2017-05-16 15:08:39 +02:00
Attilio Borello
64aa37858b added retry function to validate script 2017-05-16 14:33:06 +02:00
Attilio Borello
5348d4dccd added retry function to tests script 2017-05-16 14:33:06 +02:00
Attilio Borello
c3c599241f removed unit and integration tests from travis 2017-05-16 14:33:06 +02:00
Attilio Borello
c19432f95c clean up apt-cache in webui/Dockerfile 2017-05-16 14:33:06 +02:00
Attilio Borello
bdf4f48d78 replaced docker images with alpine if available (nginx, rabbitmq) 2017-05-16 14:33:06 +02:00
Attilio Borello
21aa0ea2da added DOCKER_VERSION variable 2017-05-16 14:33:06 +02:00
Timo Reimann
f8e7b5595b Merge pull request #1585 from timoreimann/1-3-maintain-sticky-flag-on-lb-method-validation-failure
Maintain sticky flag on LB method validation failure.
2017-05-16 00:41:15 +02:00
Timo Reimann
f9839f7b1d Turn configureBackends into method. 2017-05-16 00:06:42 +02:00
Timo Reimann
2c45428c8a Maintain sticky flag on LB method validation failure.
We previously did not copy the sticky flag if the load-balancer
method validation failed, causing enabled stickiness to be dropped in
case of a validation error (which, technically, for us is the same as a
load-balancer configuration without an explicitly set method). This
change fixes that.

A few refactorings and improvements along the way:

- Move the frontend and backend configuration steps into separate
  methods/functions for better testability.
- Include the invalid method name in the error value and avoid log
  duplication.
- Add tests for the backend configuration part.
2017-05-16 00:06:42 +02:00
Emile Vauge
30aa5a82b3 Merge pull request #1577 from aantono/Issue1569
Fixed ReplacePath rule executing out of order, when combined with PathPrefixStrip
2017-05-15 23:21:53 +02:00
Alex Antonov
3f68e382fd Fixed ReplacePath rule executing out of order, when combined with PathPrefixStrip #1569 2017-05-15 10:08:18 -05:00
Emile Vauge
9e57a283d7 Merge pull request #1601 from containous/fix-fatal-auth
Fix empty basic auth
2017-05-15 17:00:52 +02:00
Emile Vauge
eaedc1b924 Fix empty basic auth
Signed-off-by: Emile Vauge <emile@vauge.com>
2017-05-15 16:03:46 +02:00
Emile Vauge
e3ab4e4d63 Merge pull request #1598 from containous/fix-stats-hijack
Fix stats hijack
2017-05-15 15:04:23 +02:00
Emile Vauge
48a91d05b5 Add Recover tests 2017-05-15 09:17:33 +02:00
Emile Vauge
111251da05 Adds Panic Recover middleware
Signed-off-by: Emile Vauge <emile@vauge.com>
2017-05-15 09:17:33 +02:00
Emile Vauge
71cec1580b Fix stats responseRecorder Hijacker
Signed-off-by: Emile Vauge <emile@vauge.com>
2017-05-15 09:17:33 +02:00
Emile Vauge
ffe1104851 Merge pull request #1588 from containous/fix-exported-fields-providers
Fix exported fields providers
2017-05-11 22:58:00 +02:00
Emile Vauge
aa4ed088bb Unexport Kvclient & StoreType from kv Provider
Signed-off-by: Emile Vauge <emile@vauge.com>
2017-05-11 19:33:32 +02:00
Emile Vauge
3a4ec19817 Add missing description tag
Signed-off-by: Emile Vauge <emile@vauge.com>
2017-05-11 19:33:32 +02:00
Timo Reimann
d2b204a075 Merge pull request #1581 from timoreimann/1-3-kubernetes-ignore-missing-pass-host-header-annotation
[Kubernetes] Ignore missing pass host header annotation. [v1.3 - CHERRY-PICK]
2017-05-11 17:43:14 +02:00
Timo Reimann
fe6c35bc6b [Kubernetes] Ignore missing pass host header annotation.
A missing annotation would previously be handled in the default error
case, causing a noisy warning-level log message to be generated each
time.

We add another case statement to ignore the case where the annotation is
missing from the annotations map.

Also piggybacking a minor improvement to the log message.
2017-05-10 19:32:45 +02:00
100 changed files with 4679 additions and 809 deletions

View File

@@ -1,7 +1,11 @@
#!/usr/bin/env bash
set -e
sudo -E apt-get -yq update
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-ce=${DOCKER_VERSION}*
docker version
pip install --user -r requirements.txt
make pull-images
make validate
ci_retry make validate

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env bash
set -e
make test-unit && make test-integration
make test-unit
ci_retry make test-integration
make -j${N_MAKE_JOBS} crossbinary-default-parallel

View File

@@ -15,3 +15,25 @@ fi
export CODENAME=raclette
export N_MAKE_JOBS=2
function ci_retry {
local NRETRY=3
local NSLEEP=5
local n=0
until [ $n -ge $NRETRY ]
do
"$@" && break
n=$[$n+1]
echo "$@ failed, attempt ${n}/${NRETRY}"
sleep $NSLEEP
done
[ $n -lt $NRETRY ]
}
export -f ci_retry

View File

@@ -12,31 +12,18 @@ env:
- CODENAME: raclette
- N_MAKE_JOBS: 2
matrix:
fast_finish: true
include:
- env: DOCKER_VERSION=1.10.3
- env: DOCKER_VERSION=1.12.6
before_install:
- sudo -E apt-get -yq update
- sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-engine=${DOCKER_VERSION}*
install:
- docker version
- pip install --user -r requirements.txt
- make pull-images
before_script:
- make validate
script:
- make test-unit && travis_retry make test-integration
- make -j${N_MAKE_JOBS} crossbinary-default-parallel
after_failure:
- docker ps
- echo "Skipping tests... (Tests are executed on SemaphoreCI)"
before_deploy:
- >
if ! [ "$BEFORE_DEPLOY_RUN" ]; then
export BEFORE_DEPLOY_RUN=1;
make -j${N_MAKE_JOBS} crossbinary-others-parallel;
sudo -E apt-get -yq update;
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-ce=${DOCKER_VERSION}*;
docker version;
pip install --user -r requirements.txt;
make -j${N_MAKE_JOBS} crossbinary-parallel;
make image;
mkdocs build --clean;
tar cfz dist/traefik-${VERSION}.src.tar.gz --exclude-vcs --exclude dist .;

View File

@@ -1,62 +1,292 @@
# Change Log
## [v1.3.0-rc1](https://github.com/containous/traefik/tree/v1.3.0-rc1) (2017-05-04)
[Full Changelog](https://github.com/containous/traefik/compare/v1.2.3...v1.3.0-rc1)
## [v1.3.6](https://github.com/containous/traefik/tree/v1.3.6) (2017-08-20)
[All Commits](https://github.com/containous/traefik/compare/v1.3.5...v1.3.6)
**Merged pull requests:**
**Bug fixes:**
- **[oxy,websocket]** Websocket parameters and protocol. ([#1970](https://github.com/containous/traefik/pull/1970) by [ldez](https://github.com/ldez))
- Working UI [\#1542](https://github.com/containous/traefik/pull/1542) ([maxwo](https://github.com/maxwo))
- Revert "First stage of access logging middleware. Initially without … [\#1541](https://github.com/containous/traefik/pull/1541) ([emilevauge](https://github.com/emilevauge))
- Add tests lost during PR 1320. [\#1540](https://github.com/containous/traefik/pull/1540) ([timoreimann](https://github.com/timoreimann))
- Merge v1.2.3 master [\#1538](https://github.com/containous/traefik/pull/1538) ([emilevauge](https://github.com/emilevauge))
- refactor: fix for PR with master branch. [\#1537](https://github.com/containous/traefik/pull/1537) ([ldez](https://github.com/ldez))
- Makefile target to enable parallel jobs [\#1535](https://github.com/containous/traefik/pull/1535) ([atbore-phx](https://github.com/atbore-phx))
- Revert "Vendor generated file" [\#1534](https://github.com/containous/traefik/pull/1534) ([ldez](https://github.com/ldez))
- feat\(rancher\): added constraint management for rancher provider [\#1527](https://github.com/containous/traefik/pull/1527) ([yyekhlef](https://github.com/yyekhlef))
- Fix systemd watchdog feature [\#1525](https://github.com/containous/traefik/pull/1525) ([guilhem](https://github.com/guilhem))
- \[Marathon\] Bump go-marathon dep [\#1524](https://github.com/containous/traefik/pull/1524) ([jangie](https://github.com/jangie))
- Make port deterministic [\#1523](https://github.com/containous/traefik/pull/1523) ([tanyadegurechaff](https://github.com/tanyadegurechaff))
- Add unit tests for package safe [\#1517](https://github.com/containous/traefik/pull/1517) ([gottwald](https://github.com/gottwald))
- doc: small documentation review [\#1516](https://github.com/containous/traefik/pull/1516) ([ldez](https://github.com/ldez))
- doc: enhance Github templates. [\#1515](https://github.com/containous/traefik/pull/1515) ([ldez](https://github.com/ldez))
- Re-exclude /dist/traefik from .dockerignore. [\#1497](https://github.com/containous/traefik/pull/1497) ([timoreimann](https://github.com/timoreimann))
- Move Docker test provider instantiation into t.Run body. [\#1489](https://github.com/containous/traefik/pull/1489) ([timoreimann](https://github.com/timoreimann))
- Add basic auth to kubernetes provider [\#1488](https://github.com/containous/traefik/pull/1488) ([alpe](https://github.com/alpe))
- Fix Consul catalog prefix flags [\#1486](https://github.com/containous/traefik/pull/1486) ([emilevauge](https://github.com/emilevauge))
- doc: enhance GitHub template. [\#1482](https://github.com/containous/traefik/pull/1482) ([ldez](https://github.com/ldez))
- Check for explicitly defined Marathon port first. [\#1474](https://github.com/containous/traefik/pull/1474) ([timoreimann](https://github.com/timoreimann))
- Correct typo in code comment. [\#1473](https://github.com/containous/traefik/pull/1473) ([mattcollier](https://github.com/mattcollier))
- Update dockerignore to ignore dist and sites [\#1470](https://github.com/containous/traefik/pull/1470) ([vdemeester](https://github.com/vdemeester))
- Improve documentation for frontend rules. [\#1469](https://github.com/containous/traefik/pull/1469) ([timoreimann](https://github.com/timoreimann))
- Mention Traefik pronunciation in docs too. [\#1468](https://github.com/containous/traefik/pull/1468) ([timoreimann](https://github.com/timoreimann))
- Fix typo in command line help. [\#1467](https://github.com/containous/traefik/pull/1467) ([mattcollier](https://github.com/mattcollier))
- Vendor generated file [\#1464](https://github.com/containous/traefik/pull/1464) ([vdemeester](https://github.com/vdemeester))
- update wording [\#1458](https://github.com/containous/traefik/pull/1458) ([ben-st](https://github.com/ben-st))
- Fix Rancher backend left in uncommented state [\#1455](https://github.com/containous/traefik/pull/1455) ([martinbaillie](https://github.com/martinbaillie))
- Fix Rancher API pagination limits [\#1453](https://github.com/containous/traefik/pull/1453) ([martinbaillie](https://github.com/martinbaillie))
- Extract some code in packages [\#1449](https://github.com/containous/traefik/pull/1449) ([vdemeester](https://github.com/vdemeester))
- Update golang.org/x/sys to fix windows compilation [\#1448](https://github.com/containous/traefik/pull/1448) ([vdemeester](https://github.com/vdemeester))
- \[ci\] Don't run binary twice [\#1447](https://github.com/containous/traefik/pull/1447) ([vdemeester](https://github.com/vdemeester))
- Pull images before running CI [\#1445](https://github.com/containous/traefik/pull/1445) ([vdemeester](https://github.com/vdemeester))
- Extract providers to their own packages [\#1444](https://github.com/containous/traefik/pull/1444) ([vdemeester](https://github.com/vdemeester))
- Pass stripped prefix downstream as header \(\#985\) [\#1442](https://github.com/containous/traefik/pull/1442) ([martinbaillie](https://github.com/martinbaillie))
- Few refactoring around the docker provider [\#1440](https://github.com/containous/traefik/pull/1440) ([vdemeester](https://github.com/vdemeester))
- feat\(webui\): Dashboard filter [\#1437](https://github.com/containous/traefik/pull/1437) ([ldez](https://github.com/ldez))
- Updating Kubernetes tests to properly test missing endpoints code path [\#1436](https://github.com/containous/traefik/pull/1436) ([Regner](https://github.com/Regner))
- New access logger [\#1408](https://github.com/containous/traefik/pull/1408) ([rjshep](https://github.com/rjshep))
- Fix default timeouts for Marathon provider. [\#1398](https://github.com/containous/traefik/pull/1398) ([timoreimann](https://github.com/timoreimann))
- Add Path Replacement Rule [\#1374](https://github.com/containous/traefik/pull/1374) ([ssttevee](https://github.com/ssttevee))
- License 2017, Træfɪk =\> Træfik [\#1368](https://github.com/containous/traefik/pull/1368) ([emilevauge](https://github.com/emilevauge))
- Add libkv Username and Password [\#1357](https://github.com/containous/traefik/pull/1357) ([tcolgate](https://github.com/tcolgate))
- \[Marathon\] Detect proper hostname automatically. [\#1345](https://github.com/containous/traefik/pull/1345) ([diegooliveira](https://github.com/diegooliveira))
- Improve rancher provider handling of service and container health states [\#1343](https://github.com/containous/traefik/pull/1343) ([kelchm](https://github.com/kelchm))
- Fix regex with PathStrip [\#1339](https://github.com/containous/traefik/pull/1339) ([seguins](https://github.com/seguins))
- Add global health check interval parameter. [\#1338](https://github.com/containous/traefik/pull/1338) ([timoreimann](https://github.com/timoreimann))
- make docs more clear about how to work with the current api [\#1337](https://github.com/containous/traefik/pull/1337) ([SantoDE](https://github.com/SantoDE))
- Make Traefik health checks label-configurable with Marathon. [\#1320](https://github.com/containous/traefik/pull/1320) ([timoreimann](https://github.com/timoreimann))
- using more sensible consul blocking query to detect health check changes [\#1241](https://github.com/containous/traefik/pull/1241) ([vholovko](https://github.com/vholovko))
- Fix error in documentation for Docker labels [\#1179](https://github.com/containous/traefik/pull/1179) ([bgandon](https://github.com/bgandon))
- Add Basic Auth per Frontend [\#1147](https://github.com/containous/traefik/pull/1147) ([SantoDE](https://github.com/SantoDE))
## [v1.3.5](https://github.com/containous/traefik/tree/v1.3.5) (2017-08-01)
[All Commits](https://github.com/containous/traefik/compare/v1.3.4...v1.3.5)
**Bug fixes:**
- **[websocket]** Oxy with fixes on websocket + integration tests ([#1905](https://github.com/containous/traefik/pull/1905) by [Juliens](https://github.com/Juliens))
## [v1.3.4](https://github.com/containous/traefik/tree/v1.3.4) (2017-07-27)
[All Commits](https://github.com/containous/traefik/compare/v1.3.3...v1.3.4)
**Bug fixes:**
- **[middleware]** Double compression. ([#1863](https://github.com/containous/traefik/pull/1863) by [ldez](https://github.com/ldez))
- **[middleware]** Fix replace path rule ([#1859](https://github.com/containous/traefik/pull/1859) by [dedalusj](https://github.com/dedalusj))
- **[websocket]** New oxy with gorilla for websocket with integration tests ([#1896](https://github.com/containous/traefik/pull/1896) by [Juliens](https://github.com/Juliens))
## [v1.3.3](https://github.com/containous/traefik/tree/v1.3.3) (2017-07-06)
[All Commits](https://github.com/containous/traefik/compare/v1.3.2...v1.3.3)
**Bug fixes:**
- **[k8s]** Undo the Secrets controller sync wait. ([#1828](https://github.com/containous/traefik/pull/1828) by [timoreimann](https://github.com/timoreimann))
- **[k8s]** Tell glog to log everything into STDERR. ([#1817](https://github.com/containous/traefik/pull/1817) by [timoreimann](https://github.com/timoreimann))
## [v1.3.2](https://github.com/containous/traefik/tree/v1.3.2) (2017-06-29)
[All Commits](https://github.com/containous/traefik/compare/v1.3.1...v1.3.2)
**Bug fixes:**
- **[acme]** Add provided certificate checking before LE certificate generation with OnHostRule option ([#1772](https://github.com/containous/traefik/pull/1772) by [nmengin](https://github.com/nmengin))
- **[k8s]** Fix race on closing event channel. ([#1798](https://github.com/containous/traefik/pull/1798) by [timoreimann](https://github.com/timoreimann))
- **[marathon]** Upgrade go-marathon to dd6cbd4. ([#1800](https://github.com/containous/traefik/pull/1800) by [timoreimann](https://github.com/timoreimann))
- **[oxy,websocket]** Problem with keepalive when switching protocol failed ([#1782](https://github.com/containous/traefik/pull/1782) by [ldez](https://github.com/ldez))
- **[oxy]** Fix proxying of unannounced trailers ([#1805](https://github.com/containous/traefik/pull/1805) by [ldez](https://github.com/ldez))
## [v1.3.1](https://github.com/containous/traefik/tree/v1.3.1) (2017-06-16)
[All Commits](https://github.com/containous/traefik/compare/v1.3.0...v1.3.1)
**Enhancements:**
- **[logs,eureka,marathon]** Minor logs changes ([#1749](https://github.com/containous/traefik/pull/1749) by [ldez](https://github.com/ldez))
**Bug fixes:**
- **[k8s]** Use correct type when watching for k8s secrets ([#1700](https://github.com/containous/traefik/pull/1700) by [kekoav](https://github.com/kekoav))
- **[middleware]** fix: Double compression. ([#1714](https://github.com/containous/traefik/pull/1714) by [ldez](https://github.com/ldez))
- **[webui]** Don&#39;t fail when backend or frontend are empty. ([#1757](https://github.com/containous/traefik/pull/1757) by [ldez](https://github.com/ldez))
**Documentation:**
- **[k8s]** Fix capitalization of PathPrefixStrip in kubernetes doc ([#1695](https://github.com/containous/traefik/pull/1695) by [Miouge1](https://github.com/Miouge1))
## [v1.3.0](https://github.com/containous/traefik/tree/v1.3.0) (2017-05-31)
[All Commits](https://github.com/containous/traefik/compare/v1.2.0-rc1...v1.3.0)
**Enhancements:**
- **[acme]** Tighten regex match for wildcard certs [Addendum to #1018] ([#1227](https://github.com/containous/traefik/pull/1227) by [dtomcej](https://github.com/dtomcej))
- **[api,webui]** Feature web root path ([#1233](https://github.com/containous/traefik/pull/1233) by [tcoupin](https://github.com/tcoupin))
- **[authentication,docker,rancher]** Add Basic Auth per Frontend ([#1147](https://github.com/containous/traefik/pull/1147) by [SantoDE](https://github.com/SantoDE))
- **[authentication]** Allow usersFile to be specified for basic or digest auth ([#1189](https://github.com/containous/traefik/pull/1189) by [krancour](https://github.com/krancour))
- **[docker]** Allow multiple rules from docker labels containers with traefik.&lt;servicename&gt;.* properties ([#1257](https://github.com/containous/traefik/pull/1257) by [benoitf](https://github.com/benoitf))
- **[docker]** Use docker-compose labels for frontend and backend names ([#1235](https://github.com/containous/traefik/pull/1235) by [tcoupin](https://github.com/tcoupin))
- **[dynamodb]** add dynamodb backend ([#1158](https://github.com/containous/traefik/pull/1158) by [tskinn](https://github.com/tskinn))
- **[healthcheck,consul]** using more sensible consul blocking query to detect health check changes ([#1241](https://github.com/containous/traefik/pull/1241) by [vholovko](https://github.com/vholovko))
- **[healthcheck]** Add global health check interval parameter. ([#1338](https://github.com/containous/traefik/pull/1338) by [timoreimann](https://github.com/timoreimann))
- **[healthcheck]** Start health checks early. ([#1319](https://github.com/containous/traefik/pull/1319) by [timoreimann](https://github.com/timoreimann))
- **[k8s]** Upgrade k8s.io/client-go to version 2 ([#1178](https://github.com/containous/traefik/pull/1178) by [errm](https://github.com/errm))
- **[k8s]** Support cluster-external Kubernetes client. ([#1159](https://github.com/containous/traefik/pull/1159) by [timoreimann](https://github.com/timoreimann))
- **[k8s]** Add basic auth to kubernetes provider ([#1488](https://github.com/containous/traefik/pull/1488) by [alpe](https://github.com/alpe))
- **[k8s]** Adding support for Traefik to respect the K8s ingress class annotation ([#1182](https://github.com/containous/traefik/pull/1182) by [Regner](https://github.com/Regner))
- **[k8s]** Refactor k8s rule type annotation parsing/retrieval. ([#1151](https://github.com/containous/traefik/pull/1151) by [timoreimann](https://github.com/timoreimann))
- **[k8s]** Kubernetes support externalname service ([#1149](https://github.com/containous/traefik/pull/1149) by [Regner](https://github.com/Regner))
- **[kv]** Add libkv Username and Password ([#1357](https://github.com/containous/traefik/pull/1357) by [tcolgate](https://github.com/tcolgate))
- **[kv]** kv: Ignore backend servers with no url ([#1196](https://github.com/containous/traefik/pull/1196) by [klausenbusk](https://github.com/klausenbusk))
- **[logs]** New access logger ([#1408](https://github.com/containous/traefik/pull/1408) by [rjshep](https://github.com/rjshep))
- **[logs]** Revert &#34;New access logger&#34; ([#1541](https://github.com/containous/traefik/pull/1541) by [emilevauge](https://github.com/emilevauge))
- **[marathon]** Allow traefik.port to not be in the list of marathon ports ([#1394](https://github.com/containous/traefik/pull/1394) by [emilevauge](https://github.com/emilevauge))
- **[marathon]** Add tests lost during PR 1320. ([#1540](https://github.com/containous/traefik/pull/1540) by [timoreimann](https://github.com/timoreimann))
- **[marathon]** Make Traefik health checks label-configurable with Marathon. ([#1320](https://github.com/containous/traefik/pull/1320) by [timoreimann](https://github.com/timoreimann))
- **[marathon]** Detect proper hostname automatically. ([#1345](https://github.com/containous/traefik/pull/1345) by [diegooliveira](https://github.com/diegooliveira))
- **[rancher]** Added constraint management for Rancher provider ([#1527](https://github.com/containous/traefik/pull/1527) by [yyekhlef](https://github.com/yyekhlef))
- **[rancher]** Improve rancher provider handling of service and container health states ([#1343](https://github.com/containous/traefik/pull/1343) by [kelchm](https://github.com/kelchm))
- **[rancher]** Fix Rancher API pagination limits ([#1453](https://github.com/containous/traefik/pull/1453) by [martinbaillie](https://github.com/martinbaillie))
- **[rancher]** Fix Rancher backend left in uncommented state ([#1455](https://github.com/containous/traefik/pull/1455) by [martinbaillie](https://github.com/martinbaillie))
- **[rules]** Add Path Replacement Rule ([#1374](https://github.com/containous/traefik/pull/1374) by [ssttevee](https://github.com/ssttevee))
- **[rules]** Add PathStripRegex rule ([#1339](https://github.com/containous/traefik/pull/1339) by [seguins](https://github.com/seguins))
- **[webui]** Working UI ([#1542](https://github.com/containous/traefik/pull/1542) by [maxwo](https://github.com/maxwo))
- **[webui]** Dashboard filter ([#1437](https://github.com/containous/traefik/pull/1437) by [ldez](https://github.com/ldez))
- Upgrade dependencies. ([#1170](https://github.com/containous/traefik/pull/1170) by [timoreimann](https://github.com/timoreimann))
- Bump go 1.8 ([#1259](https://github.com/containous/traefik/pull/1259) by [emilevauge](https://github.com/emilevauge))
- Update TLS Ciphers for Go 1.8 ([#1276](https://github.com/containous/traefik/pull/1276) by [kekoav](https://github.com/kekoav))
- Add IdleConnTimeout to Traefik&#39;s http.server settings ([#1340](https://github.com/containous/traefik/pull/1340) by [bparli](https://github.com/bparli))
- Pass stripped prefix downstream as header ([#1442](https://github.com/containous/traefik/pull/1442) by [martinbaillie](https://github.com/martinbaillie))
- Extract some code in packages ([#1449](https://github.com/containous/traefik/pull/1449) by [vdemeester](https://github.com/vdemeester))
- Vendor generated file ([#1464](https://github.com/containous/traefik/pull/1464) by [vdemeester](https://github.com/vdemeester))
- Add unit tests for package safe ([#1517](https://github.com/containous/traefik/pull/1517) by [gottwald](https://github.com/gottwald))
- Use TOML-compatible duration type. ([#1350](https://github.com/containous/traefik/pull/1350) by [timoreimann](https://github.com/timoreimann))
- Get testify/require dependency. ([#1658](https://github.com/containous/traefik/pull/1658) by [timoreimann](https://github.com/timoreimann))
**Bug fixes:**
- **[consul]** fix consul sample endpoints ([#1303](https://github.com/containous/traefik/pull/1303) by [ruslansennov](https://github.com/ruslansennov))
- **[consul]** Fix Consul catalog prefix flags ([#1486](https://github.com/containous/traefik/pull/1486) by [emilevauge](https://github.com/emilevauge))
- **[docker]** Make port deterministic ([#1523](https://github.com/containous/traefik/pull/1523) by [tanyadegurechaff](https://github.com/tanyadegurechaff))
- **[k8s]** Remove rule type path list. ([#1630](https://github.com/containous/traefik/pull/1630) by [timoreimann](https://github.com/timoreimann))
- **[k8s]** Ignore Ingresses with empty Endpoint subsets. ([#1604](https://github.com/containous/traefik/pull/1604) by [timoreimann](https://github.com/timoreimann))
- **[k8s]** Ignore missing pass host header annotation. ([#1581](https://github.com/containous/traefik/pull/1581) by [timoreimann](https://github.com/timoreimann))
- **[logs]** Fix empty basic auth ([#1601](https://github.com/containous/traefik/pull/1601) by [emilevauge](https://github.com/emilevauge))
- **[logs]** Create log folder if not present ([#1507](https://github.com/containous/traefik/pull/1507) by [tanyadegurechaff](https://github.com/tanyadegurechaff))
- **[marathon]** Upgrade go-marathon to 15ea23e. ([#1635](https://github.com/containous/traefik/pull/1635) by [timoreimann](https://github.com/timoreimann))
- **[marathon]** Fix default timeouts for Marathon provider. ([#1398](https://github.com/containous/traefik/pull/1398) by [timoreimann](https://github.com/timoreimann))
- **[marathon]** Check for explicitly defined Marathon port first. ([#1474](https://github.com/containous/traefik/pull/1474) by [timoreimann](https://github.com/timoreimann))
- **[marathon]** Bump go-marathon dep ([#1524](https://github.com/containous/traefik/pull/1524) by [jangie](https://github.com/jangie))
- **[middleware,rules]** Fix behavior for PathPrefixStrip ([#1638](https://github.com/containous/traefik/pull/1638) by [seryl](https://github.com/seryl))
- **[middleware,websocket]** Fix stats hijack ([#1598](https://github.com/containous/traefik/pull/1598) by [emilevauge](https://github.com/emilevauge))
- **[provider]** Fix exported fields providers ([#1588](https://github.com/containous/traefik/pull/1588) by [emilevauge](https://github.com/emilevauge))
- **[rancher]** fix: Empty Rancher Service Labels. ([#1654](https://github.com/containous/traefik/pull/1654) by [ldez](https://github.com/ldez))
- **[sticky-session]** Maintain sticky flag on LB method validation failure. ([#1585](https://github.com/containous/traefik/pull/1585) by [timoreimann](https://github.com/timoreimann))
- Revert &#34;Vendor generated file&#34; ([#1534](https://github.com/containous/traefik/pull/1534) by [ldez](https://github.com/ldez))
- Update golang.org/x/sys to fix windows compilation ([#1448](https://github.com/containous/traefik/pull/1448) by [vdemeester](https://github.com/vdemeester))
- Fix systemd watchdog feature ([#1525](https://github.com/containous/traefik/pull/1525) by [guilhem](https://github.com/guilhem))
- Fixed ReplacePath rule executing out of order, when combined with PathPrefixStrip ([#1577](https://github.com/containous/traefik/pull/1577) by [aantono](https://github.com/aantono))
**Documentation:**
- **[cluster]** doc: Traefik cluster in beta. ([#1610](https://github.com/containous/traefik/pull/1610) by [ldez](https://github.com/ldez))
- **[docker]** Fix error in documentation for Docker labels ([#1179](https://github.com/containous/traefik/pull/1179) by [bgandon](https://github.com/bgandon))
- **[k8s]** Re Organise k8s docs to make 1.6 usage easier ([#1602](https://github.com/containous/traefik/pull/1602) by [errm](https://github.com/errm))
- **[k8s]** Add documentation for k8s RBAC configuration ([#1404](https://github.com/containous/traefik/pull/1404) by [aolwas](https://github.com/aolwas))
- **[k8s]** Add documentation about k8s Helm Chart ([#1367](https://github.com/containous/traefik/pull/1367) by [seguins](https://github.com/seguins))
- **[marathon]** Add Marathon guide. ([#1578](https://github.com/containous/traefik/pull/1578) by [Stibbons](https://github.com/Stibbons))
- **[metrics]** Fix prometheus metrics example ([#1157](https://github.com/containous/traefik/pull/1157) by [solidnerd](https://github.com/solidnerd))
- **[metrics]** Make toml Bucket array homogeneous ([#1369](https://github.com/containous/traefik/pull/1369) by [Starefossen](https://github.com/Starefossen))
- **[rancher]** make docs more clear about how to work with the current api ([#1337](https://github.com/containous/traefik/pull/1337) by [SantoDE](https://github.com/SantoDE))
- **[rules]** Motivate and explain regular expression rules. ([#1216](https://github.com/containous/traefik/pull/1216) by [timoreimann](https://github.com/timoreimann))
- **[rules]** Improve documentation for frontend rules. ([#1469](https://github.com/containous/traefik/pull/1469) by [timoreimann](https://github.com/timoreimann))
- License 2017, Træfɪk =&gt; Træfik ([#1368](https://github.com/containous/traefik/pull/1368) by [emilevauge](https://github.com/emilevauge))
- update wording ([#1458](https://github.com/containous/traefik/pull/1458) by [ben-st](https://github.com/ben-st))
- Fix typo in command line help. ([#1467](https://github.com/containous/traefik/pull/1467) by [mattcollier](https://github.com/mattcollier))
- Mention Traefik pronunciation in docs too. ([#1468](https://github.com/containous/traefik/pull/1468) by [timoreimann](https://github.com/timoreimann))
- Correct typo in code comment. ([#1473](https://github.com/containous/traefik/pull/1473) by [mattcollier](https://github.com/mattcollier))
- Change a word in the documentation ([#1274](https://github.com/containous/traefik/pull/1274) by [sroze](https://github.com/sroze))
- Add @trecloux to Maintainers ([#1226](https://github.com/containous/traefik/pull/1226) by [emilevauge](https://github.com/emilevauge))
- doc: enhance GitHub template. ([#1482](https://github.com/containous/traefik/pull/1482) by [ldez](https://github.com/ldez))
- Add @timoreimann to list of maintainers. ([#1215](https://github.com/containous/traefik/pull/1215) by [timoreimann](https://github.com/timoreimann))
- Add Traefik TOML sample section on how to bind to specific IP addr. ([#1194](https://github.com/containous/traefik/pull/1194) by [timoreimann](https://github.com/timoreimann))
- doc: enhance Github templates. ([#1515](https://github.com/containous/traefik/pull/1515) by [ldez](https://github.com/ldez))
- doc: small documentation review ([#1516](https://github.com/containous/traefik/pull/1516) by [ldez](https://github.com/ldez))
**Misc:**
- **[docker]** Few refactoring around the docker provider ([#1440](https://github.com/containous/traefik/pull/1440) by [vdemeester](https://github.com/vdemeester))
- **[k8s]** Updating Kubernetes tests to properly test missing endpoints code path ([#1436](https://github.com/containous/traefik/pull/1436) by [Regner](https://github.com/Regner))
- **[provider]** Extract providers to their own packages ([#1444](https://github.com/containous/traefik/pull/1444) by [vdemeester](https://github.com/vdemeester))
- Fix typo in server.go ([#1386](https://github.com/containous/traefik/pull/1386) by [mihaitodor](https://github.com/mihaitodor))
- Vendor dependencies ([#1144](https://github.com/containous/traefik/pull/1144) by [timoreimann](https://github.com/timoreimann))
- Prepare release v1.3.0-rc3 ([#1661](https://github.com/containous/traefik/pull/1661) by [ldez](https://github.com/ldez))
- Prepare release v1.3.0-rc2 ([#1606](https://github.com/containous/traefik/pull/1606) by [emilevauge](https://github.com/emilevauge))
- Prepare release v1.3.0-rc1 ([#1553](https://github.com/containous/traefik/pull/1553) by [emilevauge](https://github.com/emilevauge))
- Merge v1.2.3 master ([#1538](https://github.com/containous/traefik/pull/1538) by [emilevauge](https://github.com/emilevauge))
- Merge v1.2.1 master ([#1383](https://github.com/containous/traefik/pull/1383) by [emilevauge](https://github.com/emilevauge))
- Merge v1.2.0 rc2 master ([#1208](https://github.com/containous/traefik/pull/1208) by [emilevauge](https://github.com/emilevauge))
## [v1.3.0-rc3](https://github.com/containous/traefik/tree/v1.3.0-rc3) (2017-05-24)
[All Commits](https://github.com/containous/traefik/compare/v1.3.0-rc2...v1.3.0-rc3)
**Enhancements:**
- [#1658](https://github.com/containous/traefik/issues/1658) Get testify/require dependency. ([timoreimann](https://github.com/timoreimann))
**Bug fixes:**
- [#1507](https://github.com/containous/traefik/issues/1507) Create log folder if not present ([tanyadegurechaff](https://github.com/tanyadegurechaff))
- [#1604](https://github.com/containous/traefik/issues/1604) [k8s] Ignore Ingresses with empty Endpoint subsets. ([timoreimann](https://github.com/timoreimann))
- [#1630](https://github.com/containous/traefik/issues/1630) [k8s] Remove rule type path list. ([timoreimann](https://github.com/timoreimann))
- [#1635](https://github.com/containous/traefik/issues/1635) Upgrade go-marathon to 15ea23e. ([timoreimann](https://github.com/timoreimann))
- [#1638](https://github.com/containous/traefik/issues/1638) Fix behavior for PathPrefixStrip ([seryl](https://github.com/seryl))
- [#1654](https://github.com/containous/traefik/issues/1654) fix: Empty Rancher Service Labels. ([ldez](https://github.com/ldez))
**Documentation:**
- [#1578](https://github.com/containous/traefik/issues/1578) Add Marathon guide. ([Stibbons](https://github.com/Stibbons))
- [#1602](https://github.com/containous/traefik/issues/1602) Re Orginise k8s docs to make 1.6 usage easier ([errm](https://github.com/errm))
- [#1642](https://github.com/containous/traefik/issues/1642) Update changelog ([ldez](https://github.com/ldez))
## [v1.3.0-rc2](https://github.com/containous/traefik/tree/v1.3.0-rc2) (2017-05-16)
[All Commits](https://github.com/containous/traefik/compare/v1.3.0-rc1...v1.3.0-rc2)
**Enhancements:**
- Fixed ReplacePath rule executing out of order, when combined with PathPrefixStrip [#1577](https://github.com/containous/traefik/issues/1577) ([aantono](https://github.com/aantono))
**Bug fixes:**
- [Kubernetes] Ignore missing pass host header annotation. [#1581](https://github.com/containous/traefik/issues/1581) ([timoreimann](https://github.com/timoreimann))
- Maintain sticky flag on LB method validation failure. [#1585](https://github.com/containous/traefik/issues/1585) ([timoreimann](https://github.com/timoreimann))
- Fix exported fields providers [#1588](https://github.com/containous/traefik/issues/1588) ([emilevauge](https://github.com/emilevauge))
- Fix stats hijack [#1598](https://github.com/containous/traefik/issues/1598) ([emilevauge](https://github.com/emilevauge))
- Fix empty basic auth [#1601](https://github.com/containous/traefik/issues/1601) ([emilevauge](https://github.com/emilevauge))
**Documentation:**
- doc: Traefik cluster in beta. [#1610](https://github.com/containous/traefik/issues/1610) ([ldez](https://github.com/ldez))
## [v1.3.0-rc1](https://github.com/containous/traefik/tree/v1.3.0-rc1) (2017-05-05)
[All Commits](https://github.com/containous/traefik/compare/v1.2.0-rc1...v1.3.0-rc1)
**Enhancements:**
- Add Basic Auth per Frontend [#1147](https://github.com/containous/traefik/issues/1147) ([SantoDE](https://github.com/SantoDE))
- Kubernetes support externalname service [#1149](https://github.com/containous/traefik/issues/1149) ([Regner](https://github.com/Regner))
- add dynamodb backend [#1158](https://github.com/containous/traefik/issues/1158) ([tskinn](https://github.com/tskinn))
- Support cluster-external Kubernetes client. [#1159](https://github.com/containous/traefik/issues/1159) ([timoreimann](https://github.com/timoreimann))
- Add Traefik TOML sample section on how to bind to specific IP addr. [#1194](https://github.com/containous/traefik/issues/1194) ([timoreimann](https://github.com/timoreimann))
- kv: Ignore backend servers with no url [#1196](https://github.com/containous/traefik/issues/1196) ([klausenbusk](https://github.com/klausenbusk))
- Tighten regex match for wildcard certs [Addendum to #1018] [#1227](https://github.com/containous/traefik/issues/1227) ([dtomcej](https://github.com/dtomcej))
- Feature web root path [#1233](https://github.com/containous/traefik/issues/1233) ([tcoupin](https://github.com/tcoupin))
- using more sensible consul blocking query to detect health check changes [#1241](https://github.com/containous/traefik/issues/1241) ([vholovko](https://github.com/vholovko))
- Allow multiple rules from docker labels containers with traefik.&lt;servicename&gt;.* properties [#1257](https://github.com/containous/traefik/issues/1257) ([benoitf](https://github.com/benoitf))
- Update TLS Ciphers for Go 1.8 [#1276](https://github.com/containous/traefik/issues/1276) ([kekoav](https://github.com/kekoav))
- Start health checks early. [#1319](https://github.com/containous/traefik/issues/1319) ([timoreimann](https://github.com/timoreimann))
- Make Traefik health checks label-configurable with Marathon. [#1320](https://github.com/containous/traefik/issues/1320) ([timoreimann](https://github.com/timoreimann))
- Append template section asking for debug log output. [#1324](https://github.com/containous/traefik/issues/1324) ([timoreimann](https://github.com/timoreimann))
- Add global health check interval parameter. [#1338](https://github.com/containous/traefik/issues/1338) ([timoreimann](https://github.com/timoreimann))
- Fix regex with PathStrip [#1339](https://github.com/containous/traefik/issues/1339) ([seguins](https://github.com/seguins))
- Add IdleConnTimeout to Traefik&#39;s http.server settings [#1340](https://github.com/containous/traefik/issues/1340) ([bparli](https://github.com/bparli))
- Improve rancher provider handling of service and container health states [#1343](https://github.com/containous/traefik/issues/1343) ([kelchm](https://github.com/kelchm))
- [Marathon] Detect proper hostname automatically. [#1345](https://github.com/containous/traefik/issues/1345) ([diegooliveira](https://github.com/diegooliveira))
- Use TOML-compatible duration type. [#1350](https://github.com/containous/traefik/issues/1350) ([timoreimann](https://github.com/timoreimann))
- Add libkv Username and Password [#1357](https://github.com/containous/traefik/issues/1357) ([tcolgate](https://github.com/tcolgate))
- Make toml Bucket array homogeneous [#1369](https://github.com/containous/traefik/issues/1369) ([Starefossen](https://github.com/Starefossen))
- Add Path Replacement Rule [#1374](https://github.com/containous/traefik/issues/1374) ([ssttevee](https://github.com/ssttevee))
- New access logger [#1408](https://github.com/containous/traefik/issues/1408) ([rjshep](https://github.com/rjshep))
- feat(webui): Dashboard filter [#1437](https://github.com/containous/traefik/issues/1437) ([ldez](https://github.com/ldez))
- Pass stripped prefix downstream as header (#985) [#1442](https://github.com/containous/traefik/issues/1442) ([martinbaillie](https://github.com/martinbaillie))
- Extract some code in packages [#1449](https://github.com/containous/traefik/issues/1449) ([vdemeester](https://github.com/vdemeester))
- Fix Rancher API pagination limits [#1453](https://github.com/containous/traefik/issues/1453) ([martinbaillie](https://github.com/martinbaillie))
- Fix Rancher backend left in uncommented state [#1455](https://github.com/containous/traefik/issues/1455) ([martinbaillie](https://github.com/martinbaillie))
- Vendor generated file [#1464](https://github.com/containous/traefik/issues/1464) ([vdemeester](https://github.com/vdemeester))
- Add basic auth to kubernetes provider [#1488](https://github.com/containous/traefik/issues/1488) ([alpe](https://github.com/alpe))
- Add unit tests for package safe [#1517](https://github.com/containous/traefik/issues/1517) ([gottwald](https://github.com/gottwald))
- feat(rancher): added constraint management for rancher provider [#1527](https://github.com/containous/traefik/issues/1527) ([yyekhlef](https://github.com/yyekhlef))
- refactor: fix for PR with master branch. [#1537](https://github.com/containous/traefik/issues/1537) ([ldez](https://github.com/ldez))
- Add tests lost during PR 1320. [#1540](https://github.com/containous/traefik/issues/1540) ([timoreimann](https://github.com/timoreimann))
- Working UI [#1542](https://github.com/containous/traefik/issues/1542) ([maxwo](https://github.com/maxwo))
**Bug fixes:**
- Fix default timeouts for Marathon provider. [#1398](https://github.com/containous/traefik/issues/1398) ([timoreimann](https://github.com/timoreimann))
- Update golang.org/x/sys to fix windows compilation [#1448](https://github.com/containous/traefik/issues/1448) ([vdemeester](https://github.com/vdemeester))
- Check for explicitly defined Marathon port first. [#1474](https://github.com/containous/traefik/issues/1474) ([timoreimann](https://github.com/timoreimann))
- Fix Consul catalog prefix flags [#1486](https://github.com/containous/traefik/issues/1486) ([emilevauge](https://github.com/emilevauge))
- Move Docker test provider instantiation into t.Run body. [#1489](https://github.com/containous/traefik/issues/1489) ([timoreimann](https://github.com/timoreimann))
- Make port deterministic [#1523](https://github.com/containous/traefik/issues/1523) ([tanyadegurechaff](https://github.com/tanyadegurechaff))
- [Marathon] Bump go-marathon dep [#1524](https://github.com/containous/traefik/issues/1524) ([jangie](https://github.com/jangie))
- Fix systemd watchdog feature [#1525](https://github.com/containous/traefik/issues/1525) ([guilhem](https://github.com/guilhem))
- Revert &#34;Vendor generated file&#34; [#1534](https://github.com/containous/traefik/issues/1534) ([ldez](https://github.com/ldez))
**Documentation:**
- Fix prometheus metrics example [#1157](https://github.com/containous/traefik/issues/1157) ([solidnerd](https://github.com/solidnerd))
- Fix error in documentation for Docker labels [#1179](https://github.com/containous/traefik/issues/1179) ([bgandon](https://github.com/bgandon))
- Motivate and explain regular expression rules. [#1216](https://github.com/containous/traefik/issues/1216) ([timoreimann](https://github.com/timoreimann))
- Add @trecloux to Maintainers [#1226](https://github.com/containous/traefik/issues/1226) ([emilevauge](https://github.com/emilevauge))
- Change a word in the documentation [#1274](https://github.com/containous/traefik/issues/1274) ([sroze](https://github.com/sroze))
- make docs more clear about how to work with the current api [#1337](https://github.com/containous/traefik/issues/1337) ([SantoDE](https://github.com/SantoDE))
- Add documentation about k8s Helm Chart [#1367](https://github.com/containous/traefik/issues/1367) ([seguins](https://github.com/seguins))
- License 2017, Træfɪk =&gt; Træfik [#1368](https://github.com/containous/traefik/issues/1368) ([emilevauge](https://github.com/emilevauge))
- Add documentation for k8s RBAC configuration [#1404](https://github.com/containous/traefik/issues/1404) ([aolwas](https://github.com/aolwas))
- update wording [#1458](https://github.com/containous/traefik/issues/1458) ([ben-st](https://github.com/ben-st))
- Fix typo in command line help. [#1467](https://github.com/containous/traefik/issues/1467) ([mattcollier](https://github.com/mattcollier))
- Mention Traefik pronunciation in docs too. [#1468](https://github.com/containous/traefik/issues/1468) ([timoreimann](https://github.com/timoreimann))
- Improve documentation for frontend rules. [#1469](https://github.com/containous/traefik/issues/1469) ([timoreimann](https://github.com/timoreimann))
- Correct typo in code comment. [#1473](https://github.com/containous/traefik/issues/1473) ([mattcollier](https://github.com/mattcollier))
- doc: enhance GitHub template. [#1482](https://github.com/containous/traefik/issues/1482) ([ldez](https://github.com/ldez))
- doc: enhance Github templates. [#1515](https://github.com/containous/traefik/issues/1515) ([ldez](https://github.com/ldez))
- doc: small documentation review [#1516](https://github.com/containous/traefik/issues/1516) ([ldez](https://github.com/ldez))
**Misc:**
- Vendor dependencies [#1144](https://github.com/containous/traefik/issues/1144) ([timoreimann](https://github.com/timoreimann))
- Refactor k8s rule type annotation parsing/retrieval. [#1151](https://github.com/containous/traefik/issues/1151) ([timoreimann](https://github.com/timoreimann))
- Upgrade dependencies. [#1170](https://github.com/containous/traefik/issues/1170) ([timoreimann](https://github.com/timoreimann))
- Remove .gitattributes file. [#1172](https://github.com/containous/traefik/issues/1172) ([timoreimann](https://github.com/timoreimann))
- Upgrade k8s.io/client-go to version 2 [#1178](https://github.com/containous/traefik/issues/1178) ([errm](https://github.com/errm))
- Adding support for Traefik to respect the K8s ingress class annotation [#1182](https://github.com/containous/traefik/issues/1182) ([Regner](https://github.com/Regner))
- Allow usersFile to be specified for basic or digest auth [#1189](https://github.com/containous/traefik/issues/1189) ([krancour](https://github.com/krancour))
- Merge v1.2.0 rc2 master [#1208](https://github.com/containous/traefik/issues/1208) ([emilevauge](https://github.com/emilevauge))
- Add @timoreimann to list of maintainers. [#1215](https://github.com/containous/traefik/issues/1215) ([timoreimann](https://github.com/timoreimann))
- Use docker-compose labels for frontend and backend names [#1235](https://github.com/containous/traefik/issues/1235) ([tcoupin](https://github.com/tcoupin))
- Bump go 1.8 [#1259](https://github.com/containous/traefik/issues/1259) ([emilevauge](https://github.com/emilevauge))
- fix consul sample endpoints [#1303](https://github.com/containous/traefik/issues/1303) ([ruslansennov](https://github.com/ruslansennov))
- Merge v1.2.1 master [#1383](https://github.com/containous/traefik/issues/1383) ([emilevauge](https://github.com/emilevauge))
- Fix typo in server.go [#1386](https://github.com/containous/traefik/issues/1386) ([mihaitodor](https://github.com/mihaitodor))
- Allow traefik.port to not be in the list of marathon ports [#1394](https://github.com/containous/traefik/issues/1394) ([emilevauge](https://github.com/emilevauge))
- Updating Kubernetes tests to properly test missing endpoints code path [#1436](https://github.com/containous/traefik/issues/1436) ([Regner](https://github.com/Regner))
- Few refactoring around the docker provider [#1440](https://github.com/containous/traefik/issues/1440) ([vdemeester](https://github.com/vdemeester))
- Extract providers to their own packages [#1444](https://github.com/containous/traefik/issues/1444) ([vdemeester](https://github.com/vdemeester))
- Merge v1.2.3 master [#1538](https://github.com/containous/traefik/issues/1538) ([emilevauge](https://github.com/emilevauge))
- Revert &#34;First stage of access logging middleware. Initially without … [#1541](https://github.com/containous/traefik/issues/1541) ([emilevauge](https://github.com/emilevauge))
- Prepare release v1.3.0-rc1 [#1553](https://github.com/containous/traefik/issues/1553) ([emilevauge](https://github.com/emilevauge))
## [v1.2.3](https://github.com/containous/traefik/tree/v1.2.3) (2017-04-13)
[Full Changelog](https://github.com/containous/traefik/compare/v1.2.2...v1.2.3)
@@ -958,4 +1188,4 @@
\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
\* *This Change Log was automatically generated by [gcg](https://github.com/ldez/gcg)*

View File

@@ -328,14 +328,11 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func
func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
domain := types.CanonicalDomain(clientHello.ServerName)
account := a.store.Get().(*Account)
//use regex to test for wildcard certs that might have been added into TLSConfig
for k := range a.TLSConfig.NameToCertificate {
selector := "^" + strings.Replace(k, "*.", "[^\\.]*\\.?", -1) + "$"
match, _ := regexp.MatchString(selector, domain)
if match {
return a.TLSConfig.NameToCertificate[k], nil
}
if providedCertificate := a.getProvidedCertificate([]string{domain}); providedCertificate != nil {
return providedCertificate, nil
}
if challengeCert, ok := a.challengeProvider.getCertificate(domain); ok {
log.Debugf("ACME got challenge %s", domain)
return challengeCert, nil
@@ -520,8 +517,20 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C
// LoadCertificateForDomains loads certificates from ACME for given domains
func (a *ACME) LoadCertificateForDomains(domains []string) {
a.jobs.In() <- func() {
log.Debugf("LoadCertificateForDomains %s...", domains)
log.Debugf("LoadCertificateForDomains %v...", domains)
if len(domains) == 0 {
// no domain
return
}
domains = fun.Map(types.CanonicalDomain, domains).([]string)
// Check provided certificates
if a.getProvidedCertificate(domains) != nil {
return
}
operation := func() error {
if a.client == nil {
return fmt.Errorf("ACME client still not built")
@@ -540,11 +549,7 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
}
account := a.store.Get().(*Account)
var domain Domain
if len(domains) == 0 {
// no domain
return
} else if len(domains) > 1 {
if len(domains) > 1 {
domain = Domain{Main: domains[0], SANs: domains[1:]}
} else {
domain = Domain{Main: domains[0]}
@@ -578,6 +583,29 @@ func (a *ACME) LoadCertificateForDomains(domains []string) {
}
}
// Get provided certificate which check a domains list (Main and SANs)
func (a *ACME) getProvidedCertificate(domains []string) *tls.Certificate {
// Use regex to test for provided certs that might have been added into TLSConfig
providedCertMatch := false
log.Debugf("Look for provided certificate to validate %s...", domains)
for k := range a.TLSConfig.NameToCertificate {
selector := "^" + strings.Replace(k, "*.", "[^\\.]*\\.?", -1) + "$"
for _, domainToCheck := range domains {
providedCertMatch, _ = regexp.MatchString(selector, domainToCheck)
if !providedCertMatch {
break
}
}
if providedCertMatch {
log.Debugf("Got provided certificate for domains %s", domains)
return a.TLSConfig.NameToCertificate[k]
}
}
log.Debugf("No provided certificate found for domains %s, get ACME certificate.", domains)
return nil
}
func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) {
domains = fun.Map(types.CanonicalDomain, domains).([]string)
log.Debugf("Loading ACME certificates %s...", domains)

View File

@@ -1,6 +1,7 @@
package acme
import (
"crypto/tls"
"encoding/base64"
"net/http"
"net/http/httptest"
@@ -9,6 +10,7 @@ import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/xenolf/lego/acme"
)
@@ -277,3 +279,18 @@ cijFkALeQp/qyeXdFld2v9gUN3eCgljgcl0QweRoIc=---`)
t.Errorf("No change to acme.PreCheckDNS when meant to be adding enforcing override function.")
}
}
func TestAcme_getProvidedCertificate(t *testing.T) {
mm := make(map[string]*tls.Certificate)
mm["*.containo.us"] = &tls.Certificate{}
mm["traefik.acme.io"] = &tls.Certificate{}
a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}}
domains := []string{"traefik.containo.us", "trae.containo.us"}
certificate := a.getProvidedCertificate(domains)
assert.NotNil(t, certificate)
domains = []string{"traefik.acme.io", "trae.acme.io"}
certificate = a.getProvidedCertificate(domains)
assert.Nil(t, certificate)
}

View File

@@ -15,7 +15,7 @@ RUN go get github.com/jteeuwen/go-bindata/... \
&& go get github.com/sgotti/glide-vc
# Which docker version to test on
ARG DOCKER_VERSION=1.10.3
ARG DOCKER_VERSION=17.03.1
# Which glide version to test on
@@ -28,7 +28,7 @@ RUN mkdir -p /usr/local/bin \
# Download docker
RUN mkdir -p /usr/local/bin \
&& curl -fL https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION}.tgz \
&& curl -fL https://get.docker.com/builds/Linux/x86_64/docker-${DOCKER_VERSION}-ce.tgz \
| tar -xzC /usr/local/bin --transform 's#^.+/##x'
WORKDIR /go/src/github.com/containous/traefik

View File

@@ -7,6 +7,7 @@ import (
fmtlog "log"
"net/http"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
@@ -206,6 +207,13 @@ func run(traefikConfiguration *server.TraefikConfiguration) {
}
log.SetLevel(level)
if len(globalConfiguration.TraefikLogsFile) > 0 {
dir := filepath.Dir(globalConfiguration.TraefikLogsFile)
err := os.MkdirAll(dir, 0755)
if err != nil {
log.Errorf("Failed to create log path %s: %s", dir, err)
}
fi, err := os.OpenFile(globalConfiguration.TraefikLogsFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
defer func() {
if err := fi.Close(); err != nil {

View File

@@ -191,6 +191,25 @@ backend = "backend2"
rule = "Path:/test1,/test2"
```
### Rules Order
When combining `Modifier` rules with `Matcher` rules, it is important to remember that `Modifier` rules **ALWAYS** apply after the `Matcher` rules.
The following rules are both `Matchers` and `Modifiers`, so the `Matcher` portion of the rule will apply first, and the `Modifier` will apply later.
- `PathStrip`
- `PathStripRegex`
- `PathPrefixStrip`
- `PathPrefixStripRegex`
`Modifiers` will be applied in a pre-determined order regardless of their order in the `rule` configuration section.
1. `PathStrip`
2. `PathPrefixStrip`
3. `PathStripRegex`
4. `PathPrefixStripRegex`
5. `AddPrefix`
6. `ReplacePath`
### Priorities
By default, routes will be sorted (in descending order) using rules length (to avoid path overlap):

View File

@@ -1,4 +1,4 @@
# Clustering / High Availability
# Clustering / High Availability (beta)
This guide explains how tu use Træfik in high availability mode.
In order to deploy and configure multiple Træfik instances, without copying the same configuration file on each instance, we will use a distributed Key-Value store.
@@ -15,5 +15,6 @@ Please refer to [this section](/user-guide/kv-config/#store-configuration-in-key
## Deploy a Træfik cluster
Once your Træfik configuration is uploaded on your KV store, you can start each Træfik instance.
A Træfik cluster is based on a master/slave model. When starting, Træfik will elect a master. If this instance fails, another master will be automatically elected.
A Træfik cluster is based on a master/slave model.
When starting, Træfik will elect a master. If this instance fails, another master will be automatically elected.

View File

@@ -12,68 +12,15 @@ on your machine, as it is the quickest way to get a local Kubernetes cluster set
2. The `kubectl` binary should be [installed on your workstation](http://kubernetes.io/docs/getting-started-guides/minikube/#download-kubectl).
## Deploy Træfik using a Deployment object
### Role Based Access Control configuration (Kubernetes 1.6+ only)
We are going to deploy Træfik with a
[Deployment](http://kubernetes.io/docs/user-guide/deployments/), as this will
allow you to easily roll out config changes or update the image.
```yaml
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: traefik-ingress-controller
namespace: kube-system
labels:
k8s-app: traefik-ingress-lb
spec:
replicas: 1
selector:
matchLabels:
k8s-app: traefik-ingress-lb
template:
metadata:
labels:
k8s-app: traefik-ingress-lb
name: traefik-ingress-lb
spec:
terminationGracePeriodSeconds: 60
containers:
- image: traefik
name: traefik-ingress-lb
resources:
limits:
cpu: 200m
memory: 30Mi
requests:
cpu: 100m
memory: 20Mi
ports:
- containerPort: 80
hostPort: 80
- containerPort: 8080
args:
- --web
- --kubernetes
```
[examples/k8s/traefik.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/traefik.yaml)
> notice that we binding port 80 on the Træfik container to port 80 on the host.
> With a multi node cluster we might expose Træfik with a NodePort or LoadBalancer service
> and run more than 1 replica of Træfik for high availability.
To deploy Træfik to your cluster start by submitting the deployment to the cluster with `kubectl`:
```sh
kubectl apply -f examples/k8s/traefik.yaml
```
### Role Based Access Control configuration (optional)
Kubernetes introduces [Role Based Access Control (RBAC)](https://kubernetes.io/docs/admin/authorization/) in 1.6+ to allow fine-grained control
Kubernetes introduces [Role Based Access Control (RBAC)](https://kubernetes.io/docs/admin/authorization/rbac/) in 1.6+ to allow fine-grained control
of Kubernetes resources and api.
If your cluster is configured with RBAC, you need to authorize Traefik to use
kubernetes API using ClusterRole, ServiceAccount and ClusterRoleBinding resources:
If your cluster is configured with RBAC, you may need to authorize Traefik to use
kubernetes API using ClusterRole and ClusterRoleBinding resources:
_Note: your cluster may have suitable ClusterRoles already setup, but the following should work everywhere_
```yaml
---
@@ -101,12 +48,6 @@ rules:
- list
- watch
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: traefik-ingress-controller
namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
@@ -121,10 +62,75 @@ subjects:
namespace: kube-system
```
Then you add the service account information to Traefik deployment spec:
`serviceAccountName: traefik-ingress-controller`
[examples/k8s/traefik-rbac.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/traefik-rbac.yaml)
[examples/k8s/traefik-with-rbac.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/traefik-with-rbac.yaml)
```shell
kubectl apply -f https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/traefik-rbac.yaml
```
## Deploy Træfik using a Deployment object
We are going to deploy Træfik with a
[Deployment](http://kubernetes.io/docs/user-guide/deployments/), as this will
allow you to easily roll out config changes or update the image.
```yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: traefik-ingress-controller
namespace: kube-system
---
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: traefik-ingress-controller
namespace: kube-system
labels:
k8s-app: traefik-ingress-lb
spec:
replicas: 1
selector:
matchLabels:
k8s-app: traefik-ingress-lb
template:
metadata:
labels:
k8s-app: traefik-ingress-lb
name: traefik-ingress-lb
spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik
name: traefik-ingress-lb
resources:
limits:
cpu: 200m
memory: 30Mi
requests:
cpu: 100m
memory: 20Mi
ports:
- containerPort: 80
hostPort: 80
- containerPort: 8080
args:
- --web
- --kubernetes
```
[examples/k8s/traefik.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/traefik.yaml)
> notice that we binding port 80 on the Træfik container to port 80 on the host.
> With a multi node cluster we might expose Træfik with a NodePort or LoadBalancer service
> and run more than 1 replica of Træfik for high availability.
To deploy Træfik to your cluster start by submitting the deployment to the cluster with `kubectl`:
```shell
kubectl apply -f https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/traefik.yaml
```
### Check the deployment
@@ -132,7 +138,7 @@ Now lets check if our deployment was successful.
Start by listing the pods in the `kube-system` namespace:
```sh
```shell
$kubectl --namespace=kube-system get pods
NAME READY STATUS RESTARTS AGE
@@ -207,7 +213,7 @@ spec:
[examples/k8s/ui.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/ui.yaml)
```shell
kubectl apply -f examples/k8s/ui.yaml
kubectl apply -f https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/ui.yaml
```
Now lets setup an entry in our /etc/hosts file to route `traefik-ui.local`
@@ -334,7 +340,7 @@ spec:
[examples/k8s/cheese-deployments.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/cheese-deployments.yaml)
```shell
kubectl apply -f examples/k8s/cheese-deployments.yaml
kubectl apply -f https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/cheese-deployments.yaml
```
Next we need to setup a service for each of the cheese pods.
@@ -390,7 +396,7 @@ spec:
[examples/k8s/cheese-services.yaml](https://github.com/containous/traefik/tree/master/examples/k8s/cheese-services.yaml)
```shell
kubectl apply -f examples/k8s/cheese-services.yaml
kubectl apply -f https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/cheese-services.yaml
```
Now we can submit an ingress for the cheese websites.
@@ -431,7 +437,7 @@ spec:
> Notice that we list each hostname, and add a backend service.
```shell
kubectl apply -f examples/k8s/cheese-ingress.yaml
kubectl apply -f https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/cheese-ingress.yaml
```
Now visit the [Træfik dashboard](http://traefik-ui.local/) and you should
@@ -465,7 +471,7 @@ metadata:
name: cheeses
annotations:
kubernetes.io/ingress.class: traefik
traefik.frontend.rule.type: pathprefixstrip
traefik.frontend.rule.type: PathPrefixStrip
spec:
rules:
- host: cheeses.local
@@ -491,7 +497,7 @@ spec:
> the containers from the previous example without modification.
```shell
kubectl apply -f examples/k8s/cheeses-ingress.yaml
kubectl apply -f https://raw.githubusercontent.com/containous/traefik/master/examples/k8s/cheeses-ingress.yaml
```
```shell

View File

@@ -0,0 +1,96 @@
# Marathon
This guide explains how to integrate Marathon and operate the cluster in a reliable way from Traefik's standpoint.
# Host detection
Marathon offers multiple ways to run (Docker-containerized) applications, the most popular ones being
- BRIDGE-networked containers with dynamic high ports exposed
- HOST-networked containers with host machine ports
- containers with dedicated IP addresses ([IP-per-task](https://mesosphere.github.io/marathon/docs/ip-per-task.html)).
Traefik tries to detect the configured mode and route traffic to the right IP addresses. It is possible to force using task hosts with the `forceTaskHostname` option.
Given the complexity of the subject, it is possible that the heuristic fails. Apart from filing an issue and waiting for the feature request / bug report to get addressed, one workaround for such situations is to customize the Marathon template file to the individual needs. (Note that this does _not_ require rebuilding Traefik but only to point the `filename` configuration parameter to a customized version of the `marathon.tmpl` file on Traefik startup.)
# Port detection
Traefik also attempts to determine the right port (which is a [non-trivial matter in Marathon](https://mesosphere.github.io/marathon/docs/ports.html)). Following is the order by which Traefik tries to identify the port (the first one that yields a positive result will be used):
1. A arbitrary port specified through the `traefik.port` label.
1. The task port (possibly indexed through the `traefik.portIndex` label, otherwise the first one).
1. The port from the application's `portDefinitions` field (possibly indexed through the `traefik.portIndex` label, otherwise the first one).
1. The port from the application's `ipAddressPerTask` field (possibly indexed through the `traefik.portIndex` label, otherwise the first one).
# Achieving high availability
## Scenarios
There are three scenarios where the availability of a Marathon application could be impaired along with the risk of losing or failing requests:
- During the startup phase when Traefik already routes requests to the backend even though it has not completed its bootstrapping process yet.
- During the shutdown phase when Traefik still routes requests to the backend while the backend is already terminating.
- During a failure of the application when Traefik has not yet identified the backend as being erroneous.
The first two scenarios are common with every rolling upgrade of an application (i.e., a new version release or configuration update).
The following sub-sections describe how to resolve or mitigate each scenario.
### Startup
In general, it is possible to define [readiness checks](https://mesosphere.github.io/marathon/docs/readiness-checks.html) (available since Marathon version 1.1) per application and have Marathon take these into account during the startup phase. The idea is that each application provides an HTTP endpoint that Marathon queries periodically during an ongoing deployment in order to mark the associated readiness check result as successful if and only if the endpoint returns a response within the configured HTTP code range. As long as the check keeps failing, Marathon will not proceed with the deployment (within the configured upgrade stategy bounds).
Unfortunately, Traefik does not respect the result of the readiness check yet. Support is expected to land in a not-too-distant future release of Traefik, however, as being tracked by [issue 1559](https://github.com/containous/traefik/issues/1559).
A current mitigation strategy is to enable [retries](http://docs.traefik.io/toml/#retry-configuration) and make sure that a sufficient number of healthy application tasks exist so that one retry will likely hit one of those. Apart from its probabilistic nature, the workaround comes at the price of increased latency.
### Shutdown
It is possible to install a [termination handler](https://mesosphere.github.io/marathon/docs/health-checks.html) (available since Marathon version 1.3) with each application whose responsibility it is to delay the shutdown process long enough until the backend has been taken out of load-balancing rotation with reasonable confidence (i.e., Traefik has received an update from the Marathon event bus, recomputes the available Marathon backends, and applies the new configuration). Specifically, each termination handler should install a signal handler listening for a SIGTERM signal and implement the following steps on signal reception:
1. Disable Keep-Alive HTTP connections.
1. Keep accepting HTTP requests for a certain period of time.
1. Stop accepting new connections.
1. Finish serving any in-flight requests.
1. Shut down.
Traefik already ignores Marathon tasks whose state does not match `TASK_RUNNING`; since terminating tasks transition into the `TASK_KILLING` and eventually `TASK_KILLED` state, there is nothing further that needs to be done on Traefik's end.
How long HTTP requests should continue to be accepted in step 2 depends on how long Traefik needs to receive and process the Marathon configuration update. Under regular operational conditions, it should be on the order of seconds, with 10 seconds possibly being a good default value.
Again, configuring Traefik to do retries (as discussed in the previous section) can serve as a decent workaround strategy. Paired with termination handlers, they would cover for those cases where either the termination sequence or Traefik cannot complete their part of the orchestration process in time.
### Failure
A failing application always happens unexpectedly, and hence, it is very difficult or even impossible to rule out the adversal effects categorically. Failure reasons vary broadly and could stretch from unacceptable slowness, a task crash, or a network split.
There are two mitigaton efforts:
1. Configure [Marathon health checks](https://mesosphere.github.io/marathon/docs/health-checks.html) on each application.
1. Configure Traefik health checks (possibly via the `traefik.backend.healthcheck.*` labels) and make sure they probe with proper frequency.
The Marathon health check makes sure that applications once deemed dysfunctional are being rescheduled to different slaves. However, they might take a while to get triggered and the follow-up processes to complete. For that reason, the Treafik health check provides an additional check that responds more rapidly and does not require a configuration reload to happen. Additionally, it protects from cases that the Marathon health check may not be able to cover, such as a network split.
## (Non-)Alternatives
There are a few alternatives of varying quality that are frequently asked for. The remaining section is going to explore them along with a benefit/cost trade-off.
### Reusing Marathon health checks
It may seem obvious to reuse the Marathon health checks as a signal to Traefik whether an application should be taken into load-balancing rotation or not.
Apart from the increased latency a failing health check may have, a major problem with this is is that Marathon does not persist the health check results. Consequently, if a master re-election occurs in the Marathon clusters, all health check results will revert to the _unknown_ state, effectively causing all applications inside the cluster to become unavailable and leading to a complete cluster failure. Re-elections do not only happen during regular maintenance work (often requiring rolling upgrades of the Marathon nodes) but also when the Marathon leader fails spontaneously). As such, there is no way to handle this situation deterministically.
Finally, Marathon health checks are not mandatory (the default is to use the task state as reported by Mesos), so requiring them for Traefik would raise the entry barrier for Marathon users.
Traefik used to use the health check results but moved away from it as [users reported the dramatic consequences](https://github.com/containous/traefik/issues/653).
### Draining
Another common approach is to let a proxy drain backends that are supposed to shut down. That is, once a backend is supposed to shut down, Traefik would stop forwarding requests.
On the plus side, this would not require any modifications to the application in question. However, implementing this fully within Traefik seems like a non-trivial undertaking. Additionally, the approach is less flexible compared to a custom termination handler since only the latter allows for the implementation of custom termination sequences that go beyond simple request draining (e.g., persisting a snapshot state to disk prior to terminating).
The feature is currently not implemented; a request for draining in general is at [issue 41](https://github.com/containous/traefik/issues/41).

View File

@@ -3,7 +3,7 @@ kind: Ingress
metadata:
name: cheeses
annotations:
traefik.frontend.rule.type: pathprefixstrip
traefik.frontend.rule.type: PathPrefixStrip
spec:
rules:
- host: cheeses.local

View File

@@ -0,0 +1,37 @@
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: traefik-ingress-controller
rules:
- apiGroups:
- ""
resources:
- pods
- services
- endpoints
verbs:
- get
- list
- watch
- apiGroups:
- extensions
resources:
- ingresses
verbs:
- get
- list
- watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: traefik-ingress-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: traefik-ingress-controller
subjects:
- kind: ServiceAccount
name: traefik-ingress-controller
namespace: kube-system

View File

@@ -1,87 +0,0 @@
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: traefik-ingress-controller
rules:
- apiGroups:
- ""
resources:
- pods
- services
- endpoints
verbs:
- get
- list
- watch
- apiGroups:
- extensions
resources:
- ingresses
verbs:
- get
- list
- watch
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: traefik-ingress-controller
namespace: kube-system
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: traefik-ingress-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: traefik-ingress-controller
subjects:
- kind: ServiceAccount
name: traefik-ingress-controller
namespace: kube-system
---
apiVersion: v1
kind: Deployment
apiVersion: extensions/v1beta1
metadata:
name: traefik-ingress-controller
namespace: kube-system
labels:
k8s-app: traefik-ingress-lb
spec:
replicas: 1
selector:
matchLabels:
k8s-app: traefik-ingress-lb
template:
metadata:
labels:
k8s-app: traefik-ingress-lb
name: traefik-ingress-lb
spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
hostNetwork: true
containers:
- image: traefik
name: traefik-ingress-lb
resources:
limits:
cpu: 200m
memory: 30Mi
requests:
cpu: 100m
memory: 20Mi
ports:
- name: http
containerPort: 80
hostPort: 80
- name: admin
containerPort: 8081
args:
- -d
- --web
- --web.address=:8081
- --kubernetes

View File

@@ -1,5 +1,11 @@
---
apiVersion: v1
kind: Deployment
kind: ServiceAccount
metadata:
name: traefik-ingress-controller
namespace: kube-system
---
kind: DaemonSet
apiVersion: extensions/v1beta1
metadata:
name: traefik-ingress-controller
@@ -7,16 +13,13 @@ metadata:
labels:
k8s-app: traefik-ingress-lb
spec:
replicas: 1
selector:
matchLabels:
k8s-app: traefik-ingress-lb
template:
metadata:
labels:
k8s-app: traefik-ingress-lb
name: traefik-ingress-lb
spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
hostNetwork: true
containers:
@@ -35,6 +38,8 @@ spec:
hostPort: 80
- name: admin
containerPort: 8081
securityContext:
privileged: true
args:
- -d
- --web

17
glide.lock generated
View File

@@ -1,5 +1,5 @@
hash: 1aa32496b865dda72d76c7cba3458f1c2c467acf0b99aab4609323f109aa64f6
updated: 2017-05-02T11:46:23.91434995-04:00
hash: 8b1dbedc74488a75ba15557befbf44cf56060bc562e7407ef9a09791ec5cabd6
updated: 2017-08-18T16:47:14.848940186+02:00
imports:
- name: cloud.google.com/go
version: 2e6a95edb1071d750f6d7db777bf66cd2997af6c
@@ -178,7 +178,7 @@ imports:
- store/etcd
- store/zookeeper
- name: github.com/donovanhide/eventsource
version: fd1de70867126402be23c306e1ce32828455d85b
version: 441a03aa37b3329bbb79f43de81914ea18724718
- name: github.com/eapache/channels
version: 47238d5aae8c0fefd518ef2bee46290909cf8263
- name: github.com/eapache/queue
@@ -201,7 +201,7 @@ imports:
- name: github.com/fatih/color
version: 9131ab34cf20d2f6d83fdc67168a5430d1c7dc23
- name: github.com/gambol99/go-marathon
version: d672c6fbb499596869d95146a26e7d0746c06c54
version: dd6cbd4c2d71294a19fb89158f2a00d427f174ab
- name: github.com/ghodss/yaml
version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee
- name: github.com/go-ini/ini
@@ -246,7 +246,7 @@ imports:
- name: github.com/gorilla/context
version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42
- name: github.com/gorilla/websocket
version: a91eba7f97777409bc2c443f5534d41dd20c5720
version: a69d9f6de432e2c6b296a947d8a5ee88f68522cf
- name: github.com/hashicorp/consul
version: 3f92cc70e8163df866873c16c6d89889b5c95fc4
subpackages:
@@ -320,7 +320,9 @@ imports:
- name: github.com/mvdan/xurls
version: db96455566f05ffe42bd6ac671f05eeb1152b45d
- name: github.com/NYTimes/gziphandler
version: 22d4470af89e09998fc16b35029df973932df4ae
version: 316adfc72ed3b0157975917adf62ba2dc31842ce
repo: https://github.com/containous/gziphandler.git
vcs: git
- name: github.com/ogier/pflag
version: 45c278ab3607870051a2ea9040bb85fcb8557481
- name: github.com/opencontainers/runc
@@ -391,6 +393,7 @@ imports:
subpackages:
- assert
- mock
- require
- name: github.com/thoas/stats
version: 152b5d051953fdb6e45f14b6826962aadc032324
- name: github.com/timewasted/linode
@@ -408,7 +411,7 @@ imports:
- name: github.com/vdemeester/docker-events
version: be74d4929ec1ad118df54349fda4b0cba60f849b
- name: github.com/vulcand/oxy
version: f88530866c561d24a6b5aac49f76d6351b788b9f
version: 321ed9ffd102941c736731b8e7bae811820d267d
repo: https://github.com/containous/oxy.git
vcs: git
subpackages:

View File

@@ -8,7 +8,7 @@ import:
- package: github.com/cenk/backoff
- package: github.com/containous/flaeg
- package: github.com/vulcand/oxy
version: f88530866c561d24a6b5aac49f76d6351b788b9f
version: 321ed9ffd102941c736731b8e7bae811820d267d
repo: https://github.com/containous/oxy.git
vcs: git
subpackages:
@@ -49,7 +49,9 @@ import:
- package: github.com/streamrail/concurrent-map
- package: github.com/stretchr/testify
subpackages:
- assert
- mock
- require
- package: github.com/thoas/stats
version: 152b5d051953fdb6e45f14b6826962aadc032324
- package: github.com/unrolled/render
@@ -85,13 +87,15 @@ import:
vcs: git
- package: github.com/abbot/go-http-auth
- package: github.com/NYTimes/gziphandler
repo: https://github.com/containous/gziphandler.git
vcs: git
- package: github.com/docker/leadership
- package: github.com/satori/go.uuid
version: ^1.1.0
- package: k8s.io/client-go
version: v2.0.0
- package: github.com/gambol99/go-marathon
version: d672c6fbb499596869d95146a26e7d0746c06c54
version: dd6cbd4c2d71294a19fb89158f2a00d427f174ab
- package: github.com/ArthurHlt/go-eureka-client
subpackages:
- eureka

View File

@@ -2,32 +2,45 @@ package main
import (
"crypto/tls"
"errors"
"net/http"
"os"
"os/exec"
"time"
"github.com/go-check/check"
"errors"
"github.com/containous/traefik/integration/utils"
"github.com/go-check/check"
checker "github.com/vdemeester/shakers"
)
// ACME test suites (using libcompose)
type AcmeSuite struct {
BaseSuite
boulderIP string
}
// Acme tests configuration
type AcmeTestCase struct {
onDemand bool
traefikConfFilePath string
domainToCheck string
}
// Domain to check
const acmeDomain = "traefik.acme.wtf"
// Wildcard domain to chekc
const wildcardDomain = "*.acme.wtf"
func (s *AcmeSuite) SetUpSuite(c *check.C) {
s.createComposeProject(c, "boulder")
s.composeProject.Start(c)
boulderHost := s.composeProject.Container(c, "boulder").NetworkSettings.IPAddress
s.boulderIP = s.composeProject.Container(c, "boulder").NetworkSettings.IPAddress
// wait for boulder
err := utils.Try(120*time.Second, func() error {
resp, err := http.Get("http://" + boulderHost + ":4000/directory")
resp, err := http.Get("http://" + s.boulderIP + ":4000/directory")
if err != nil {
return err
}
@@ -47,9 +60,48 @@ func (s *AcmeSuite) TearDownSuite(c *check.C) {
}
}
func (s *AcmeSuite) TestRetrieveAcmeCertificate(c *check.C) {
boulderHost := s.composeProject.Container(c, "boulder").NetworkSettings.IPAddress
file := s.adaptFile(c, "fixtures/acme/acme.toml", struct{ BoulderHost string }{boulderHost})
// Test OnDemand option with none provided certificate
func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificate(c *check.C) {
aTestCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/acme.toml",
onDemand: true,
domainToCheck: acmeDomain}
s.retrieveAcmeCertificate(c, aTestCase)
}
// Test OnHostRule option with none provided certificate
func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificate(c *check.C) {
aTestCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/acme.toml",
onDemand: false,
domainToCheck: acmeDomain}
s.retrieveAcmeCertificate(c, aTestCase)
}
// Test OnDemand option with a wildcard provided certificate
func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateWithWildcard(c *check.C) {
aTestCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_provided.toml",
onDemand: true,
domainToCheck: wildcardDomain}
s.retrieveAcmeCertificate(c, aTestCase)
}
// Test onHostRule option with a wildcard provided certificate
func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithWildcard(c *check.C) {
aTestCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_provided.toml",
onDemand: false,
domainToCheck: wildcardDomain}
s.retrieveAcmeCertificate(c, aTestCase)
}
// Doing an HTTPS request and test the response certificate
func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, a AcmeTestCase) {
file := s.adaptFile(c, a.traefikConfFilePath, struct {
BoulderHost string
OnDemand, OnHostRule bool
}{s.boulderIP, a.onDemand, !a.onDemand})
defer os.Remove(file)
cmd := exec.Command(traefikBinary, "--configFile="+file)
err := cmd.Start()
@@ -77,16 +129,32 @@ func (s *AcmeSuite) TestRetrieveAcmeCertificate(c *check.C) {
tr = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
ServerName: "traefik.acme.wtf",
ServerName: acmeDomain,
},
}
client = &http.Client{Transport: tr}
req, _ := http.NewRequest("GET", "https://127.0.0.1:5001/", nil)
req.Host = "traefik.acme.wtf"
req.Header.Set("Host", "traefik.acme.wtf")
req.Host = acmeDomain
req.Header.Set("Host", acmeDomain)
req.Header.Set("Accept", "*/*")
resp, err := client.Do(req)
var resp *http.Response
// Retry to send a Request which uses the LE generated certificate
err = utils.Try(60*time.Second, func() error {
resp, err = client.Do(req)
// /!\ If connection is not closed, SSLHandshake will only be done during the first trial /!\
req.Close = true
if err != nil {
return err
} else if resp.TLS.PeerCertificates[0].Subject.CommonName != a.domainToCheck {
return errors.New("Domain " + resp.TLS.PeerCertificates[0].Subject.CommonName + " found in place of " + a.domainToCheck)
}
return nil
})
c.Assert(err, checker.IsNil)
// Check Domain into response certificate
c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, checker.Equals, a.domainToCheck)
// Expected a 200
c.Assert(resp.StatusCode, checker.Equals, 200)
}

View File

@@ -0,0 +1,37 @@
# How to generate the self-signed wildcard certificate
```bash
#!/usr/bin/env bash
# Specify where we will install
# the wildcard certificate
SSL_DIR="./ssl"
# Set the wildcarded domain
# we want to use
DOMAIN="*.acme.wtf"
# A blank passphrase
PASSPHRASE=""
# Set our CSR variables
SUBJ="
C=FR
ST=MP
O=
localityName=Toulouse
commonName=$DOMAIN
organizationalUnitName=Traefik
emailAddress=
"
# Create our SSL directory
# in case it doesn't exist
sudo mkdir -p "$SSL_DIR"
# Generate our Private Key, CSR and Certificate
sudo openssl genrsa -out "$SSL_DIR/wildcard.key" 2048
sudo openssl req -new -subj "$(echo -n "$SUBJ" | tr "\n" "/")" -key "$SSL_DIR/wildcard.key" -out "$SSL_DIR/wildcard.csr" -passin pass:$PASSPHRASE
sudo openssl x509 -req -days 3650 -in "$SSL_DIR/wildcard.csr" -signkey "$SSL_DIR/wildcard.key" -out "$SSL_DIR/wildcard.crt"
sudo rm -f "$SSL_DIR/wildcard.csr"
```

View File

@@ -14,7 +14,8 @@ defaultEntryPoints = ["http", "https"]
email = "test@traefik.io"
storage = "/dev/null"
entryPoint = "https"
onDemand = true
onDemand = {{.OnDemand}}
OnHostRule = {{.OnHostRule}}
caServer = "http://{{.BoulderHost}}:4000/directory"
[file]

View File

@@ -0,0 +1,35 @@
logLevel = "DEBUG"
defaultEntryPoints = ["http", "https"]
[entryPoints]
[entryPoints.http]
address = ":8080"
[entryPoints.https]
address = ":5001"
[entryPoints.https.tls]
[[entryPoints.https.tls.certificates]]
CertFile = "fixtures/acme/ssl/wildcard.crt"
KeyFile = "fixtures/acme/ssl/wildcard.key"
[acme]
email = "test@traefik.io"
storage = "/dev/null"
entryPoint = "https"
onDemand = {{.OnDemand}}
OnHostRule = {{.OnHostRule}}
caServer = "http://{{.BoulderHost}}:4000/directory"
[file]
[backends]
[backends.backend]
[backends.backend.servers.server1]
url = "http://127.0.0.1:9010"
[frontends]
[frontends.frontend]
backend = "backend"
[frontends.frontend.routes.test]
rule = "Host:traefik.acme.wtf"

View File

@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDJDCCAgwCCQCS90TE7NuTqzANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJG
UjELMAkGA1UECAwCTVAxETAPBgNVBAcMCFRvdWxvdXNlMRMwEQYDVQQDDAoqLmFj
bWUud3RmMRAwDgYDVQQLDAdUcmFlZmlrMB4XDTE3MDYyMzE0NTE0MVoXDTI3MDYy
MTE0NTE0MVowVDELMAkGA1UEBhMCRlIxCzAJBgNVBAgMAk1QMREwDwYDVQQHDAhU
b3Vsb3VzZTETMBEGA1UEAwwKKi5hY21lLnd0ZjEQMA4GA1UECwwHVHJhZWZpazCC
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAODqsVCLhauFZPhPXqZDIKST
wqoJST+jO5O/WmA7oC4S6JlecRoNsHAXyddd3cQW3yZqB0ryOHrMOpMX0PPXf3jS
OOXoXA6xsq+RXlR4hDrBkOrj/LR/g62Eiuj2JVO2uy6tKJIetSB/Wzl6OgRkY/um
EXIc7zQS81/QKg+pg7Z4AYJht5J88nOFHJ3RspUMaH1vJ6LhH3MOUkgFj+I1OiqX
Tnkd7EDWbkYxAJa0xI2qbmY5VYv8dsIUN+IlPFDtBt87Fc2qv5dQkOz11FDYxWnz
+kxX6+MESLBaTvJjXvG+bzTfh9xCExFQFiN+Us0JuLX8HKQ4MqWL2IiVLsko2osC
AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAl2jTX2yzUpiufrJ6WtZjKIAH8GF817hS
dWvt2eyLrBPvllMUj8zqCE5uNVUDVuXQvOhOyx+3zZzfcgfYqbTD8G8amNWcSiRA
vonoOn1p1pW2OonSi32h3qv5i4gCyh/6cBneYi03lkQ7uLCsJK9+dXTAvoKL6s23
IXhZGS0Qkvs4vkORA2MX9tyJdyfCCaCx3GpPCGkKrKJ8ePTEvq1ZE2xdhERnV5pz
L1PRY2QthXXVjMz7AXw0gkHvAbtrKVKR1Tv4ZK34bFBh/kyGAjkcn0zdeFKITqTF
tCoXWEArmiRqGuXwbqU3mEA9Cv6aMM+0YX89K2InhOnBU80OWs0uMQ==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA4OqxUIuFq4Vk+E9epkMgpJPCqglJP6M7k79aYDugLhLomV5x
Gg2wcBfJ113dxBbfJmoHSvI4esw6kxfQ89d/eNI45ehcDrGyr5FeVHiEOsGQ6uP8
tH+DrYSK6PYlU7a7Lq0okh61IH9bOXo6BGRj+6YRchzvNBLzX9AqD6mDtngBgmG3
knzyc4UcndGylQxofW8nouEfcw5SSAWP4jU6KpdOeR3sQNZuRjEAlrTEjapuZjlV
i/x2whQ34iU8UO0G3zsVzaq/l1CQ7PXUUNjFafP6TFfr4wRIsFpO8mNe8b5vNN+H
3EITEVAWI35SzQm4tfwcpDgypYvYiJUuySjaiwIDAQABAoIBAQCs9Ex9v4x+pQlL
2NzTxXLom6dp0dI92WwK5W696Zv3UhsDNRiMDFLNH73amxfZnizjAU2yWCkOZNX2
Hq5TlDc11ZJjWRbRRdw+He8HzdUAybCCr+a3dgbv+6hGFGIHydCOyCEWm/50ivq/
bDoI/pnT/ZQUyCM5TAlSeGSfvp7GRHi9v3HOl85H1Pn2Dvyk9gj4y3BIFrKuv8fJ
o6aEzlfgWGROCzshU2m8fB9P0B4hWDlJsc1D01sW60zhjLo9+XoWznmw5mczz7sc
S5sdDh47rSJsNRuFd7YDjeLzJWPqLrKVB5nn6nRbvrnBqhfsknkO4VIXhmEMSs1u
RMYOJ9ShAoGBAPinA6ktIeez1t5IsfxGwbCeZzFI1suZqZeX6ezNKaMpeykyAPuh
CqN7H+a4NCKsinsgHJowU98ckHeAsQ22s7R8dFZhyxEXkcBawY2soK29eq2aJHnY
lqKOwjOA7wgElRHwLkNFniQ5lKFPMly8a9NVAqg+Th/J3uR+7wE2t+b1AoGBAOeQ
H/vVkdaNB2ovnCxMh+OfxpcjkfF6KnD2jpn/TKsbR5BtnrtyRLc5+qt52D0CEgSy
qU3zrsZebShej3OIBPrEwIcPN+LezaxnLMf9RXdOde+wWrQLWLkShJaSTwSoGqZB
fcO0/sc1lzhGxm++ByP5mWbHr/VM9IdTQQH5Bct/AoGBAMhmOrIXeNL4Az2FU0Vi
dWp2T+7NqKfRAXj264Z5V4xzuxpZfadPhHZ7nhth7Erhyn4vRD4UoxQXPmvB4XCP
Bkh5YX3ZNUNiPorL2mDnd1xvcLcHm0xEfisnaWb/DCbnIomhjHeVXT4O1jYn0Qwi
o7hgNFMKXAaMuUJo9xGAWzkdAoGASxC4nY2tOiz7k1udt+qTPqHj4cjhHbOpoHb8
4UUWmH0+ZL50b3Vqey8raH0WMSjDqIw2QBPXu2yO3EBTJnOYkaZIdz/isQPjDplf
tfEPnM5tgubbcHQhLdWn75u8S9km0nB2kYPR98gSnmarGzwx2mKmbOAc1Vs+BcRi
VX5hd4cCgYAubBq0VsFT0KVU3Rva3dgPR1K5bp4r4hE5cGXm4HvLiOgv995CwPy1
27eONF9GN7hvjI6C17jA1Gyx5sN0QrsMv/1BZqiGaragMOPXFD+tVecWuKH4lZQi
VbKTOWHlGkrDCpiYWpfetQAjouj+0c6d+wigcoC8e5dwxBPI2f3rGw==
-----END RSA PRIVATE KEY-----

View File

@@ -0,0 +1,24 @@
defaultEntryPoints = ["http"]
logLevel = "DEBUG"
[entryPoints]
[entryPoints.http]
address = ":8000"
[web]
address = ":8080"
[file]
[backends]
[backends.backend1]
[backends.backend1.servers.server1]
url = "{{ .WebsocketServer }}"
[frontends]
[frontends.frontend1]
backend = "backend1"
[frontends.frontend1.routes.test_1]
rule = "Path:/ws"

View File

@@ -1,4 +1,4 @@
hash: c53f57a45247b08a91f127ece494d49f1b7fee8c5f75be87ab12e27aa92d065f
hash: afc00e3aa064550eed7ff6a98b4eced543fe41e84894f1ac0ec25adf06354ec7
updated: 2016-11-17T16:23:56.727970904Z
imports:
- name: github.com/cenk/backoff
@@ -286,6 +286,7 @@ testImports:
- context
- proxy
- publicsuffix
- websocket
- name: golang.org/x/sys
version: 9c60d1c508f5134d1ca726b4641db998f2523357
subpackages:

View File

@@ -29,5 +29,6 @@ testImport:
- package: golang.org/x/net
subpackages:
- context
- websocket
- package: github.com/spf13/pflag
version: 5644820622454e71517561946e3d94b9f9db6842

View File

@@ -14,6 +14,8 @@ import (
"github.com/containous/traefik/integration/utils"
"github.com/go-check/check"
"bytes"
compose "github.com/libkermit/compose/check"
checker "github.com/vdemeester/shakers"
)
@@ -38,6 +40,7 @@ func init() {
check.Suite(&EurekaSuite{})
check.Suite(&AcmeSuite{})
check.Suite(&DynamoDBSuite{})
check.Suite(&WebsocketSuite{})
}
var traefikBinary = "../dist/traefik"
@@ -71,6 +74,18 @@ func (s *BaseSuite) createComposeProject(c *check.C, name string) {
s.composeProject = compose.CreateProject(c, projectName, composeFile)
}
func withConfigFile(file string) string {
return "--configFile=" + file
}
func (s *BaseSuite) cmdTraefik(args ...string) (*exec.Cmd, *bytes.Buffer) {
cmd := exec.Command(traefikBinary, args...)
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &out
return cmd, &out
}
func (s *BaseSuite) traefikCmd(c *check.C, args ...string) (*exec.Cmd, string) {
cmd, out, err := utils.RunCommand(traefikBinary, args...)
c.Assert(err, checker.IsNil, check.Commentf("Fail to run %s with %v", traefikBinary, args))

View File

@@ -39,6 +39,6 @@ bmysql:
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
log_driver: none
brabbitmq:
image: rabbitmq:3
image: rabbitmq:3-alpine
environment:
RABBITMQ_NODE_IP_ADDRESS: "0.0.0.0"

View File

@@ -12,6 +12,6 @@ consul:
- "8302"
- "8302/udp"
nginx:
image: nginx
image: nginx:alpine
ports:
- "8881:80"

View File

@@ -12,6 +12,6 @@ consul:
- "8302"
- "8302/udp"
nginx:
image: nginx
image: nginx:alpine
ports:
- "8881:80"

View File

@@ -1,20 +1,20 @@
nginx1:
image: nginx
image: nginx:alpine
ports:
- "8881:80"
nginx2:
image: nginx
image: nginx:alpine
ports:
- "8882:80"
nginx3:
image: nginx
image: nginx:alpine
ports:
- "8883:80"
nginx4:
image: nginx
image: nginx:alpine
ports:
- "8884:80"
nginx5:
image: nginx
image: nginx:alpine
ports:
- "8885:80"

113
integration/vendor/golang.org/x/net/websocket/client.go generated vendored Normal file
View File

@@ -0,0 +1,113 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bufio"
"crypto/tls"
"io"
"net"
"net/http"
"net/url"
)
// DialError is an error that occurs while dialling a websocket server.
type DialError struct {
*Config
Err error
}
func (e *DialError) Error() string {
return "websocket.Dial " + e.Config.Location.String() + ": " + e.Err.Error()
}
// NewConfig creates a new WebSocket config for client connection.
func NewConfig(server, origin string) (config *Config, err error) {
config = new(Config)
config.Version = ProtocolVersionHybi13
config.Location, err = url.ParseRequestURI(server)
if err != nil {
return
}
config.Origin, err = url.ParseRequestURI(origin)
if err != nil {
return
}
config.Header = http.Header(make(map[string][]string))
return
}
// NewClient creates a new WebSocket client connection over rwc.
func NewClient(config *Config, rwc io.ReadWriteCloser) (ws *Conn, err error) {
br := bufio.NewReader(rwc)
bw := bufio.NewWriter(rwc)
err = hybiClientHandshake(config, br, bw)
if err != nil {
return
}
buf := bufio.NewReadWriter(br, bw)
ws = newHybiClientConn(config, buf, rwc)
return
}
// Dial opens a new client connection to a WebSocket.
func Dial(url_, protocol, origin string) (ws *Conn, err error) {
config, err := NewConfig(url_, origin)
if err != nil {
return nil, err
}
if protocol != "" {
config.Protocol = []string{protocol}
}
return DialConfig(config)
}
var portMap = map[string]string{
"ws": "80",
"wss": "443",
}
func parseAuthority(location *url.URL) string {
if _, ok := portMap[location.Scheme]; ok {
if _, _, err := net.SplitHostPort(location.Host); err != nil {
return net.JoinHostPort(location.Host, portMap[location.Scheme])
}
}
return location.Host
}
// DialConfig opens a new client connection to a WebSocket with a config.
func DialConfig(config *Config) (ws *Conn, err error) {
var client net.Conn
if config.Location == nil {
return nil, &DialError{config, ErrBadWebSocketLocation}
}
if config.Origin == nil {
return nil, &DialError{config, ErrBadWebSocketOrigin}
}
switch config.Location.Scheme {
case "ws":
client, err = net.Dial("tcp", parseAuthority(config.Location))
case "wss":
client, err = tls.Dial("tcp", parseAuthority(config.Location), config.TlsConfig)
default:
err = ErrBadScheme
}
if err != nil {
goto Error
}
ws, err = NewClient(config, client)
if err != nil {
client.Close()
goto Error
}
return
Error:
return nil, &DialError{config, err}
}

586
integration/vendor/golang.org/x/net/websocket/hybi.go generated vendored Normal file
View File

@@ -0,0 +1,586 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
// This file implements a protocol of hybi draft.
// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17
import (
"bufio"
"bytes"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"encoding/binary"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
)
const (
websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
closeStatusNormal = 1000
closeStatusGoingAway = 1001
closeStatusProtocolError = 1002
closeStatusUnsupportedData = 1003
closeStatusFrameTooLarge = 1004
closeStatusNoStatusRcvd = 1005
closeStatusAbnormalClosure = 1006
closeStatusBadMessageData = 1007
closeStatusPolicyViolation = 1008
closeStatusTooBigData = 1009
closeStatusExtensionMismatch = 1010
maxControlFramePayloadLength = 125
)
var (
ErrBadMaskingKey = &ProtocolError{"bad masking key"}
ErrBadPongMessage = &ProtocolError{"bad pong message"}
ErrBadClosingStatus = &ProtocolError{"bad closing status"}
ErrUnsupportedExtensions = &ProtocolError{"unsupported extensions"}
ErrNotImplemented = &ProtocolError{"not implemented"}
handshakeHeader = map[string]bool{
"Host": true,
"Upgrade": true,
"Connection": true,
"Sec-Websocket-Key": true,
"Sec-Websocket-Origin": true,
"Sec-Websocket-Version": true,
"Sec-Websocket-Protocol": true,
"Sec-Websocket-Accept": true,
}
)
// A hybiFrameHeader is a frame header as defined in hybi draft.
type hybiFrameHeader struct {
Fin bool
Rsv [3]bool
OpCode byte
Length int64
MaskingKey []byte
data *bytes.Buffer
}
// A hybiFrameReader is a reader for hybi frame.
type hybiFrameReader struct {
reader io.Reader
header hybiFrameHeader
pos int64
length int
}
func (frame *hybiFrameReader) Read(msg []byte) (n int, err error) {
n, err = frame.reader.Read(msg)
if err != nil {
return 0, err
}
if frame.header.MaskingKey != nil {
for i := 0; i < n; i++ {
msg[i] = msg[i] ^ frame.header.MaskingKey[frame.pos%4]
frame.pos++
}
}
return n, err
}
func (frame *hybiFrameReader) PayloadType() byte { return frame.header.OpCode }
func (frame *hybiFrameReader) HeaderReader() io.Reader {
if frame.header.data == nil {
return nil
}
if frame.header.data.Len() == 0 {
return nil
}
return frame.header.data
}
func (frame *hybiFrameReader) TrailerReader() io.Reader { return nil }
func (frame *hybiFrameReader) Len() (n int) { return frame.length }
// A hybiFrameReaderFactory creates new frame reader based on its frame type.
type hybiFrameReaderFactory struct {
*bufio.Reader
}
// NewFrameReader reads a frame header from the connection, and creates new reader for the frame.
// See Section 5.2 Base Framing protocol for detail.
// http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-17#section-5.2
func (buf hybiFrameReaderFactory) NewFrameReader() (frame frameReader, err error) {
hybiFrame := new(hybiFrameReader)
frame = hybiFrame
var header []byte
var b byte
// First byte. FIN/RSV1/RSV2/RSV3/OpCode(4bits)
b, err = buf.ReadByte()
if err != nil {
return
}
header = append(header, b)
hybiFrame.header.Fin = ((header[0] >> 7) & 1) != 0
for i := 0; i < 3; i++ {
j := uint(6 - i)
hybiFrame.header.Rsv[i] = ((header[0] >> j) & 1) != 0
}
hybiFrame.header.OpCode = header[0] & 0x0f
// Second byte. Mask/Payload len(7bits)
b, err = buf.ReadByte()
if err != nil {
return
}
header = append(header, b)
mask := (b & 0x80) != 0
b &= 0x7f
lengthFields := 0
switch {
case b <= 125: // Payload length 7bits.
hybiFrame.header.Length = int64(b)
case b == 126: // Payload length 7+16bits
lengthFields = 2
case b == 127: // Payload length 7+64bits
lengthFields = 8
}
for i := 0; i < lengthFields; i++ {
b, err = buf.ReadByte()
if err != nil {
return
}
if lengthFields == 8 && i == 0 { // MSB must be zero when 7+64 bits
b &= 0x7f
}
header = append(header, b)
hybiFrame.header.Length = hybiFrame.header.Length*256 + int64(b)
}
if mask {
// Masking key. 4 bytes.
for i := 0; i < 4; i++ {
b, err = buf.ReadByte()
if err != nil {
return
}
header = append(header, b)
hybiFrame.header.MaskingKey = append(hybiFrame.header.MaskingKey, b)
}
}
hybiFrame.reader = io.LimitReader(buf.Reader, hybiFrame.header.Length)
hybiFrame.header.data = bytes.NewBuffer(header)
hybiFrame.length = len(header) + int(hybiFrame.header.Length)
return
}
// A HybiFrameWriter is a writer for hybi frame.
type hybiFrameWriter struct {
writer *bufio.Writer
header *hybiFrameHeader
}
func (frame *hybiFrameWriter) Write(msg []byte) (n int, err error) {
var header []byte
var b byte
if frame.header.Fin {
b |= 0x80
}
for i := 0; i < 3; i++ {
if frame.header.Rsv[i] {
j := uint(6 - i)
b |= 1 << j
}
}
b |= frame.header.OpCode
header = append(header, b)
if frame.header.MaskingKey != nil {
b = 0x80
} else {
b = 0
}
lengthFields := 0
length := len(msg)
switch {
case length <= 125:
b |= byte(length)
case length < 65536:
b |= 126
lengthFields = 2
default:
b |= 127
lengthFields = 8
}
header = append(header, b)
for i := 0; i < lengthFields; i++ {
j := uint((lengthFields - i - 1) * 8)
b = byte((length >> j) & 0xff)
header = append(header, b)
}
if frame.header.MaskingKey != nil {
if len(frame.header.MaskingKey) != 4 {
return 0, ErrBadMaskingKey
}
header = append(header, frame.header.MaskingKey...)
frame.writer.Write(header)
data := make([]byte, length)
for i := range data {
data[i] = msg[i] ^ frame.header.MaskingKey[i%4]
}
frame.writer.Write(data)
err = frame.writer.Flush()
return length, err
}
frame.writer.Write(header)
frame.writer.Write(msg)
err = frame.writer.Flush()
return length, err
}
func (frame *hybiFrameWriter) Close() error { return nil }
type hybiFrameWriterFactory struct {
*bufio.Writer
needMaskingKey bool
}
func (buf hybiFrameWriterFactory) NewFrameWriter(payloadType byte) (frame frameWriter, err error) {
frameHeader := &hybiFrameHeader{Fin: true, OpCode: payloadType}
if buf.needMaskingKey {
frameHeader.MaskingKey, err = generateMaskingKey()
if err != nil {
return nil, err
}
}
return &hybiFrameWriter{writer: buf.Writer, header: frameHeader}, nil
}
type hybiFrameHandler struct {
conn *Conn
payloadType byte
}
func (handler *hybiFrameHandler) HandleFrame(frame frameReader) (frameReader, error) {
if handler.conn.IsServerConn() {
// The client MUST mask all frames sent to the server.
if frame.(*hybiFrameReader).header.MaskingKey == nil {
handler.WriteClose(closeStatusProtocolError)
return nil, io.EOF
}
} else {
// The server MUST NOT mask all frames.
if frame.(*hybiFrameReader).header.MaskingKey != nil {
handler.WriteClose(closeStatusProtocolError)
return nil, io.EOF
}
}
if header := frame.HeaderReader(); header != nil {
io.Copy(ioutil.Discard, header)
}
switch frame.PayloadType() {
case ContinuationFrame:
frame.(*hybiFrameReader).header.OpCode = handler.payloadType
case TextFrame, BinaryFrame:
handler.payloadType = frame.PayloadType()
case CloseFrame:
return nil, io.EOF
case PingFrame, PongFrame:
b := make([]byte, maxControlFramePayloadLength)
n, err := io.ReadFull(frame, b)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return nil, err
}
io.Copy(ioutil.Discard, frame)
if frame.PayloadType() == PingFrame {
if _, err := handler.WritePong(b[:n]); err != nil {
return nil, err
}
}
return nil, nil
}
return frame, nil
}
func (handler *hybiFrameHandler) WriteClose(status int) (err error) {
handler.conn.wio.Lock()
defer handler.conn.wio.Unlock()
w, err := handler.conn.frameWriterFactory.NewFrameWriter(CloseFrame)
if err != nil {
return err
}
msg := make([]byte, 2)
binary.BigEndian.PutUint16(msg, uint16(status))
_, err = w.Write(msg)
w.Close()
return err
}
func (handler *hybiFrameHandler) WritePong(msg []byte) (n int, err error) {
handler.conn.wio.Lock()
defer handler.conn.wio.Unlock()
w, err := handler.conn.frameWriterFactory.NewFrameWriter(PongFrame)
if err != nil {
return 0, err
}
n, err = w.Write(msg)
w.Close()
return n, err
}
// newHybiConn creates a new WebSocket connection speaking hybi draft protocol.
func newHybiConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn {
if buf == nil {
br := bufio.NewReader(rwc)
bw := bufio.NewWriter(rwc)
buf = bufio.NewReadWriter(br, bw)
}
ws := &Conn{config: config, request: request, buf: buf, rwc: rwc,
frameReaderFactory: hybiFrameReaderFactory{buf.Reader},
frameWriterFactory: hybiFrameWriterFactory{
buf.Writer, request == nil},
PayloadType: TextFrame,
defaultCloseStatus: closeStatusNormal}
ws.frameHandler = &hybiFrameHandler{conn: ws}
return ws
}
// generateMaskingKey generates a masking key for a frame.
func generateMaskingKey() (maskingKey []byte, err error) {
maskingKey = make([]byte, 4)
if _, err = io.ReadFull(rand.Reader, maskingKey); err != nil {
return
}
return
}
// generateNonce generates a nonce consisting of a randomly selected 16-byte
// value that has been base64-encoded.
func generateNonce() (nonce []byte) {
key := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, key); err != nil {
panic(err)
}
nonce = make([]byte, 24)
base64.StdEncoding.Encode(nonce, key)
return
}
// removeZone removes IPv6 zone identifer from host.
// E.g., "[fe80::1%en0]:8080" to "[fe80::1]:8080"
func removeZone(host string) string {
if !strings.HasPrefix(host, "[") {
return host
}
i := strings.LastIndex(host, "]")
if i < 0 {
return host
}
j := strings.LastIndex(host[:i], "%")
if j < 0 {
return host
}
return host[:j] + host[i:]
}
// getNonceAccept computes the base64-encoded SHA-1 of the concatenation of
// the nonce ("Sec-WebSocket-Key" value) with the websocket GUID string.
func getNonceAccept(nonce []byte) (expected []byte, err error) {
h := sha1.New()
if _, err = h.Write(nonce); err != nil {
return
}
if _, err = h.Write([]byte(websocketGUID)); err != nil {
return
}
expected = make([]byte, 28)
base64.StdEncoding.Encode(expected, h.Sum(nil))
return
}
// Client handshake described in draft-ietf-hybi-thewebsocket-protocol-17
func hybiClientHandshake(config *Config, br *bufio.Reader, bw *bufio.Writer) (err error) {
bw.WriteString("GET " + config.Location.RequestURI() + " HTTP/1.1\r\n")
// According to RFC 6874, an HTTP client, proxy, or other
// intermediary must remove any IPv6 zone identifier attached
// to an outgoing URI.
bw.WriteString("Host: " + removeZone(config.Location.Host) + "\r\n")
bw.WriteString("Upgrade: websocket\r\n")
bw.WriteString("Connection: Upgrade\r\n")
nonce := generateNonce()
if config.handshakeData != nil {
nonce = []byte(config.handshakeData["key"])
}
bw.WriteString("Sec-WebSocket-Key: " + string(nonce) + "\r\n")
bw.WriteString("Origin: " + strings.ToLower(config.Origin.String()) + "\r\n")
if config.Version != ProtocolVersionHybi13 {
return ErrBadProtocolVersion
}
bw.WriteString("Sec-WebSocket-Version: " + fmt.Sprintf("%d", config.Version) + "\r\n")
if len(config.Protocol) > 0 {
bw.WriteString("Sec-WebSocket-Protocol: " + strings.Join(config.Protocol, ", ") + "\r\n")
}
// TODO(ukai): send Sec-WebSocket-Extensions.
err = config.Header.WriteSubset(bw, handshakeHeader)
if err != nil {
return err
}
bw.WriteString("\r\n")
if err = bw.Flush(); err != nil {
return err
}
resp, err := http.ReadResponse(br, &http.Request{Method: "GET"})
if err != nil {
return err
}
if resp.StatusCode != 101 {
return ErrBadStatus
}
if strings.ToLower(resp.Header.Get("Upgrade")) != "websocket" ||
strings.ToLower(resp.Header.Get("Connection")) != "upgrade" {
return ErrBadUpgrade
}
expectedAccept, err := getNonceAccept(nonce)
if err != nil {
return err
}
if resp.Header.Get("Sec-WebSocket-Accept") != string(expectedAccept) {
return ErrChallengeResponse
}
if resp.Header.Get("Sec-WebSocket-Extensions") != "" {
return ErrUnsupportedExtensions
}
offeredProtocol := resp.Header.Get("Sec-WebSocket-Protocol")
if offeredProtocol != "" {
protocolMatched := false
for i := 0; i < len(config.Protocol); i++ {
if config.Protocol[i] == offeredProtocol {
protocolMatched = true
break
}
}
if !protocolMatched {
return ErrBadWebSocketProtocol
}
config.Protocol = []string{offeredProtocol}
}
return nil
}
// newHybiClientConn creates a client WebSocket connection after handshake.
func newHybiClientConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser) *Conn {
return newHybiConn(config, buf, rwc, nil)
}
// A HybiServerHandshaker performs a server handshake using hybi draft protocol.
type hybiServerHandshaker struct {
*Config
accept []byte
}
func (c *hybiServerHandshaker) ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error) {
c.Version = ProtocolVersionHybi13
if req.Method != "GET" {
return http.StatusMethodNotAllowed, ErrBadRequestMethod
}
// HTTP version can be safely ignored.
if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" ||
!strings.Contains(strings.ToLower(req.Header.Get("Connection")), "upgrade") {
return http.StatusBadRequest, ErrNotWebSocket
}
key := req.Header.Get("Sec-Websocket-Key")
if key == "" {
return http.StatusBadRequest, ErrChallengeResponse
}
version := req.Header.Get("Sec-Websocket-Version")
switch version {
case "13":
c.Version = ProtocolVersionHybi13
default:
return http.StatusBadRequest, ErrBadWebSocketVersion
}
var scheme string
if req.TLS != nil {
scheme = "wss"
} else {
scheme = "ws"
}
c.Location, err = url.ParseRequestURI(scheme + "://" + req.Host + req.URL.RequestURI())
if err != nil {
return http.StatusBadRequest, err
}
protocol := strings.TrimSpace(req.Header.Get("Sec-Websocket-Protocol"))
if protocol != "" {
protocols := strings.Split(protocol, ",")
for i := 0; i < len(protocols); i++ {
c.Protocol = append(c.Protocol, strings.TrimSpace(protocols[i]))
}
}
c.accept, err = getNonceAccept([]byte(key))
if err != nil {
return http.StatusInternalServerError, err
}
return http.StatusSwitchingProtocols, nil
}
// Origin parses the Origin header in req.
// If the Origin header is not set, it returns nil and nil.
func Origin(config *Config, req *http.Request) (*url.URL, error) {
var origin string
switch config.Version {
case ProtocolVersionHybi13:
origin = req.Header.Get("Origin")
}
if origin == "" {
return nil, nil
}
return url.ParseRequestURI(origin)
}
func (c *hybiServerHandshaker) AcceptHandshake(buf *bufio.Writer) (err error) {
if len(c.Protocol) > 0 {
if len(c.Protocol) != 1 {
// You need choose a Protocol in Handshake func in Server.
return ErrBadWebSocketProtocol
}
}
buf.WriteString("HTTP/1.1 101 Switching Protocols\r\n")
buf.WriteString("Upgrade: websocket\r\n")
buf.WriteString("Connection: Upgrade\r\n")
buf.WriteString("Sec-WebSocket-Accept: " + string(c.accept) + "\r\n")
if len(c.Protocol) > 0 {
buf.WriteString("Sec-WebSocket-Protocol: " + c.Protocol[0] + "\r\n")
}
// TODO(ukai): send Sec-WebSocket-Extensions.
if c.Header != nil {
err := c.Header.WriteSubset(buf, handshakeHeader)
if err != nil {
return err
}
}
buf.WriteString("\r\n")
return buf.Flush()
}
func (c *hybiServerHandshaker) NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn {
return newHybiServerConn(c.Config, buf, rwc, request)
}
// newHybiServerConn returns a new WebSocket connection speaking hybi draft protocol.
func newHybiServerConn(config *Config, buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) *Conn {
return newHybiConn(config, buf, rwc, request)
}

113
integration/vendor/golang.org/x/net/websocket/server.go generated vendored Normal file
View File

@@ -0,0 +1,113 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package websocket
import (
"bufio"
"fmt"
"io"
"net/http"
)
func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request, config *Config, handshake func(*Config, *http.Request) error) (conn *Conn, err error) {
var hs serverHandshaker = &hybiServerHandshaker{Config: config}
code, err := hs.ReadHandshake(buf.Reader, req)
if err == ErrBadWebSocketVersion {
fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion)
buf.WriteString("\r\n")
buf.WriteString(err.Error())
buf.Flush()
return
}
if err != nil {
fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
buf.WriteString("\r\n")
buf.WriteString(err.Error())
buf.Flush()
return
}
if handshake != nil {
err = handshake(config, req)
if err != nil {
code = http.StatusForbidden
fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
buf.WriteString("\r\n")
buf.Flush()
return
}
}
err = hs.AcceptHandshake(buf.Writer)
if err != nil {
code = http.StatusBadRequest
fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code))
buf.WriteString("\r\n")
buf.Flush()
return
}
conn = hs.NewServerConn(buf, rwc, req)
return
}
// Server represents a server of a WebSocket.
type Server struct {
// Config is a WebSocket configuration for new WebSocket connection.
Config
// Handshake is an optional function in WebSocket handshake.
// For example, you can check, or don't check Origin header.
// Another example, you can select config.Protocol.
Handshake func(*Config, *http.Request) error
// Handler handles a WebSocket connection.
Handler
}
// ServeHTTP implements the http.Handler interface for a WebSocket
func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
s.serveWebSocket(w, req)
}
func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) {
rwc, buf, err := w.(http.Hijacker).Hijack()
if err != nil {
panic("Hijack failed: " + err.Error())
}
// The server should abort the WebSocket connection if it finds
// the client did not send a handshake that matches with protocol
// specification.
defer rwc.Close()
conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake)
if err != nil {
return
}
if conn == nil {
panic("unexpected nil conn")
}
s.Handler(conn)
}
// Handler is a simple interface to a WebSocket browser client.
// It checks if Origin header is valid URL by default.
// You might want to verify websocket.Conn.Config().Origin in the func.
// If you use Server instead of Handler, you could call websocket.Origin and
// check the origin in your Handshake func. So, if you want to accept
// non-browser clients, which do not send an Origin header, set a
// Server.Handshake that does not check the origin.
type Handler func(*Conn)
func checkOrigin(config *Config, req *http.Request) (err error) {
config.Origin, err = Origin(config, req)
if err == nil && config.Origin == nil {
return fmt.Errorf("null origin")
}
return err
}
// ServeHTTP implements the http.Handler interface for a WebSocket
func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
s := Server{Handler: h, Handshake: checkOrigin}
s.serveWebSocket(w, req)
}

View File

@@ -0,0 +1,412 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package websocket implements a client and server for the WebSocket protocol
// as specified in RFC 6455.
package websocket // import "golang.org/x/net/websocket"
import (
"bufio"
"crypto/tls"
"encoding/json"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"sync"
"time"
)
const (
ProtocolVersionHybi13 = 13
ProtocolVersionHybi = ProtocolVersionHybi13
SupportedProtocolVersion = "13"
ContinuationFrame = 0
TextFrame = 1
BinaryFrame = 2
CloseFrame = 8
PingFrame = 9
PongFrame = 10
UnknownFrame = 255
)
// ProtocolError represents WebSocket protocol errors.
type ProtocolError struct {
ErrorString string
}
func (err *ProtocolError) Error() string { return err.ErrorString }
var (
ErrBadProtocolVersion = &ProtocolError{"bad protocol version"}
ErrBadScheme = &ProtocolError{"bad scheme"}
ErrBadStatus = &ProtocolError{"bad status"}
ErrBadUpgrade = &ProtocolError{"missing or bad upgrade"}
ErrBadWebSocketOrigin = &ProtocolError{"missing or bad WebSocket-Origin"}
ErrBadWebSocketLocation = &ProtocolError{"missing or bad WebSocket-Location"}
ErrBadWebSocketProtocol = &ProtocolError{"missing or bad WebSocket-Protocol"}
ErrBadWebSocketVersion = &ProtocolError{"missing or bad WebSocket Version"}
ErrChallengeResponse = &ProtocolError{"mismatch challenge/response"}
ErrBadFrame = &ProtocolError{"bad frame"}
ErrBadFrameBoundary = &ProtocolError{"not on frame boundary"}
ErrNotWebSocket = &ProtocolError{"not websocket protocol"}
ErrBadRequestMethod = &ProtocolError{"bad method"}
ErrNotSupported = &ProtocolError{"not supported"}
)
// Addr is an implementation of net.Addr for WebSocket.
type Addr struct {
*url.URL
}
// Network returns the network type for a WebSocket, "websocket".
func (addr *Addr) Network() string { return "websocket" }
// Config is a WebSocket configuration
type Config struct {
// A WebSocket server address.
Location *url.URL
// A Websocket client origin.
Origin *url.URL
// WebSocket subprotocols.
Protocol []string
// WebSocket protocol version.
Version int
// TLS config for secure WebSocket (wss).
TlsConfig *tls.Config
// Additional header fields to be sent in WebSocket opening handshake.
Header http.Header
handshakeData map[string]string
}
// serverHandshaker is an interface to handle WebSocket server side handshake.
type serverHandshaker interface {
// ReadHandshake reads handshake request message from client.
// Returns http response code and error if any.
ReadHandshake(buf *bufio.Reader, req *http.Request) (code int, err error)
// AcceptHandshake accepts the client handshake request and sends
// handshake response back to client.
AcceptHandshake(buf *bufio.Writer) (err error)
// NewServerConn creates a new WebSocket connection.
NewServerConn(buf *bufio.ReadWriter, rwc io.ReadWriteCloser, request *http.Request) (conn *Conn)
}
// frameReader is an interface to read a WebSocket frame.
type frameReader interface {
// Reader is to read payload of the frame.
io.Reader
// PayloadType returns payload type.
PayloadType() byte
// HeaderReader returns a reader to read header of the frame.
HeaderReader() io.Reader
// TrailerReader returns a reader to read trailer of the frame.
// If it returns nil, there is no trailer in the frame.
TrailerReader() io.Reader
// Len returns total length of the frame, including header and trailer.
Len() int
}
// frameReaderFactory is an interface to creates new frame reader.
type frameReaderFactory interface {
NewFrameReader() (r frameReader, err error)
}
// frameWriter is an interface to write a WebSocket frame.
type frameWriter interface {
// Writer is to write payload of the frame.
io.WriteCloser
}
// frameWriterFactory is an interface to create new frame writer.
type frameWriterFactory interface {
NewFrameWriter(payloadType byte) (w frameWriter, err error)
}
type frameHandler interface {
HandleFrame(frame frameReader) (r frameReader, err error)
WriteClose(status int) (err error)
}
// Conn represents a WebSocket connection.
type Conn struct {
config *Config
request *http.Request
buf *bufio.ReadWriter
rwc io.ReadWriteCloser
rio sync.Mutex
frameReaderFactory
frameReader
wio sync.Mutex
frameWriterFactory
frameHandler
PayloadType byte
defaultCloseStatus int
}
// Read implements the io.Reader interface:
// it reads data of a frame from the WebSocket connection.
// if msg is not large enough for the frame data, it fills the msg and next Read
// will read the rest of the frame data.
// it reads Text frame or Binary frame.
func (ws *Conn) Read(msg []byte) (n int, err error) {
ws.rio.Lock()
defer ws.rio.Unlock()
again:
if ws.frameReader == nil {
frame, err := ws.frameReaderFactory.NewFrameReader()
if err != nil {
return 0, err
}
ws.frameReader, err = ws.frameHandler.HandleFrame(frame)
if err != nil {
return 0, err
}
if ws.frameReader == nil {
goto again
}
}
n, err = ws.frameReader.Read(msg)
if err == io.EOF {
if trailer := ws.frameReader.TrailerReader(); trailer != nil {
io.Copy(ioutil.Discard, trailer)
}
ws.frameReader = nil
goto again
}
return n, err
}
// Write implements the io.Writer interface:
// it writes data as a frame to the WebSocket connection.
func (ws *Conn) Write(msg []byte) (n int, err error) {
ws.wio.Lock()
defer ws.wio.Unlock()
w, err := ws.frameWriterFactory.NewFrameWriter(ws.PayloadType)
if err != nil {
return 0, err
}
n, err = w.Write(msg)
w.Close()
if err != nil {
return n, err
}
return n, err
}
// Close implements the io.Closer interface.
func (ws *Conn) Close() error {
err := ws.frameHandler.WriteClose(ws.defaultCloseStatus)
err1 := ws.rwc.Close()
if err != nil {
return err
}
return err1
}
func (ws *Conn) IsClientConn() bool { return ws.request == nil }
func (ws *Conn) IsServerConn() bool { return ws.request != nil }
// LocalAddr returns the WebSocket Origin for the connection for client, or
// the WebSocket location for server.
func (ws *Conn) LocalAddr() net.Addr {
if ws.IsClientConn() {
return &Addr{ws.config.Origin}
}
return &Addr{ws.config.Location}
}
// RemoteAddr returns the WebSocket location for the connection for client, or
// the Websocket Origin for server.
func (ws *Conn) RemoteAddr() net.Addr {
if ws.IsClientConn() {
return &Addr{ws.config.Location}
}
return &Addr{ws.config.Origin}
}
var errSetDeadline = errors.New("websocket: cannot set deadline: not using a net.Conn")
// SetDeadline sets the connection's network read & write deadlines.
func (ws *Conn) SetDeadline(t time.Time) error {
if conn, ok := ws.rwc.(net.Conn); ok {
return conn.SetDeadline(t)
}
return errSetDeadline
}
// SetReadDeadline sets the connection's network read deadline.
func (ws *Conn) SetReadDeadline(t time.Time) error {
if conn, ok := ws.rwc.(net.Conn); ok {
return conn.SetReadDeadline(t)
}
return errSetDeadline
}
// SetWriteDeadline sets the connection's network write deadline.
func (ws *Conn) SetWriteDeadline(t time.Time) error {
if conn, ok := ws.rwc.(net.Conn); ok {
return conn.SetWriteDeadline(t)
}
return errSetDeadline
}
// Config returns the WebSocket config.
func (ws *Conn) Config() *Config { return ws.config }
// Request returns the http request upgraded to the WebSocket.
// It is nil for client side.
func (ws *Conn) Request() *http.Request { return ws.request }
// Codec represents a symmetric pair of functions that implement a codec.
type Codec struct {
Marshal func(v interface{}) (data []byte, payloadType byte, err error)
Unmarshal func(data []byte, payloadType byte, v interface{}) (err error)
}
// Send sends v marshaled by cd.Marshal as single frame to ws.
func (cd Codec) Send(ws *Conn, v interface{}) (err error) {
data, payloadType, err := cd.Marshal(v)
if err != nil {
return err
}
ws.wio.Lock()
defer ws.wio.Unlock()
w, err := ws.frameWriterFactory.NewFrameWriter(payloadType)
if err != nil {
return err
}
_, err = w.Write(data)
w.Close()
return err
}
// Receive receives single frame from ws, unmarshaled by cd.Unmarshal and stores in v.
func (cd Codec) Receive(ws *Conn, v interface{}) (err error) {
ws.rio.Lock()
defer ws.rio.Unlock()
if ws.frameReader != nil {
_, err = io.Copy(ioutil.Discard, ws.frameReader)
if err != nil {
return err
}
ws.frameReader = nil
}
again:
frame, err := ws.frameReaderFactory.NewFrameReader()
if err != nil {
return err
}
frame, err = ws.frameHandler.HandleFrame(frame)
if err != nil {
return err
}
if frame == nil {
goto again
}
payloadType := frame.PayloadType()
data, err := ioutil.ReadAll(frame)
if err != nil {
return err
}
return cd.Unmarshal(data, payloadType, v)
}
func marshal(v interface{}) (msg []byte, payloadType byte, err error) {
switch data := v.(type) {
case string:
return []byte(data), TextFrame, nil
case []byte:
return data, BinaryFrame, nil
}
return nil, UnknownFrame, ErrNotSupported
}
func unmarshal(msg []byte, payloadType byte, v interface{}) (err error) {
switch data := v.(type) {
case *string:
*data = string(msg)
return nil
case *[]byte:
*data = msg
return nil
}
return ErrNotSupported
}
/*
Message is a codec to send/receive text/binary data in a frame on WebSocket connection.
To send/receive text frame, use string type.
To send/receive binary frame, use []byte type.
Trivial usage:
import "websocket"
// receive text frame
var message string
websocket.Message.Receive(ws, &message)
// send text frame
message = "hello"
websocket.Message.Send(ws, message)
// receive binary frame
var data []byte
websocket.Message.Receive(ws, &data)
// send binary frame
data = []byte{0, 1, 2}
websocket.Message.Send(ws, data)
*/
var Message = Codec{marshal, unmarshal}
func jsonMarshal(v interface{}) (msg []byte, payloadType byte, err error) {
msg, err = json.Marshal(v)
return msg, TextFrame, err
}
func jsonUnmarshal(msg []byte, payloadType byte, v interface{}) (err error) {
return json.Unmarshal(msg, v)
}
/*
JSON is a codec to send/receive JSON data in a frame from a WebSocket connection.
Trivial usage:
import "websocket"
type T struct {
Msg string
Count int
}
// receive JSON type T
var data T
websocket.JSON.Receive(ws, &data)
// send JSON type T
websocket.JSON.Send(ws, data)
*/
var JSON = Codec{jsonMarshal, jsonUnmarshal}

View File

@@ -0,0 +1,275 @@
package main
import (
"errors"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"os"
"strings"
"time"
"github.com/containous/traefik/integration/utils"
"github.com/go-check/check"
gorillawebsocket "github.com/gorilla/websocket"
checker "github.com/vdemeester/shakers"
"golang.org/x/net/websocket"
)
// WebsocketSuite
type WebsocketSuite struct{ BaseSuite }
func (suite *WebsocketSuite) TestBase(c *check.C) {
var upgrader = gorillawebsocket.Upgrader{} // use default options
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := suite.adaptFile(c, "fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
defer os.Remove(file)
cmd, _ := suite.cmdTraefik(withConfigFile(file), "--debug")
err := cmd.Start()
c.Assert(err, check.IsNil)
defer cmd.Process.Kill()
// wait for traefik
err = utils.TryRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, func(res *http.Response) error {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
if !strings.Contains(string(body), "127.0.0.1") {
return errors.New("Incorrect traefik config")
}
return nil
})
c.Assert(err, checker.IsNil)
conn, _, err := gorillawebsocket.DefaultDialer.Dial("ws://127.0.0.1:8000/ws", nil)
c.Assert(err, checker.IsNil)
conn.WriteMessage(gorillawebsocket.TextMessage, []byte("OK"))
_, msg, err := conn.ReadMessage()
c.Assert(err, checker.IsNil)
c.Assert(string(msg), checker.Equals, "OK")
}
func (suite *WebsocketSuite) TestWrongOrigin(c *check.C) {
var upgrader = gorillawebsocket.Upgrader{} // use default options
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := suite.adaptFile(c, "fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
defer os.Remove(file)
cmd, _ := suite.cmdTraefik(withConfigFile(file), "--debug")
err := cmd.Start()
c.Assert(err, check.IsNil)
defer cmd.Process.Kill()
// wait for traefik
err = utils.TryRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, func(res *http.Response) error {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
if !strings.Contains(string(body), "127.0.0.1") {
return errors.New("Incorrect traefik config")
}
return nil
})
c.Assert(err, checker.IsNil)
config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:800")
c.Assert(err, check.IsNil)
conn, err := net.DialTimeout("tcp", "127.0.0.1:8000", time.Second)
_, err = websocket.NewClient(config, conn)
c.Assert(err, checker.NotNil)
c.Assert(err, checker.ErrorMatches, "bad status")
}
func (suite *WebsocketSuite) TestOrigin(c *check.C) {
var upgrader = gorillawebsocket.Upgrader{} // use default options
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := suite.adaptFile(c, "fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
defer os.Remove(file)
cmd, _ := suite.cmdTraefik(withConfigFile(file), "--debug")
err := cmd.Start()
c.Assert(err, check.IsNil)
defer cmd.Process.Kill()
// wait for traefik
err = utils.TryRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, func(res *http.Response) error {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
if !strings.Contains(string(body), "127.0.0.1") {
return errors.New("Incorrect traefik config")
}
return nil
})
c.Assert(err, checker.IsNil)
config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:8000")
c.Assert(err, check.IsNil)
conn, err := net.DialTimeout("tcp", "127.0.0.1:8000", time.Second)
client, err := websocket.NewClient(config, conn)
c.Assert(err, checker.IsNil)
n, err := client.Write([]byte("OK"))
c.Assert(err, checker.IsNil)
c.Assert(n, checker.Equals, 2)
msg := make([]byte, 2)
n, err = client.Read(msg)
c.Assert(err, checker.IsNil)
c.Assert(n, checker.Equals, 2)
c.Assert(string(msg), checker.Equals, "OK")
}
func (suite *WebsocketSuite) TestWrongOriginIgnoredByServer(c *check.C) {
var upgrader = gorillawebsocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
return true
}}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer c.Close()
for {
mt, message, err := c.ReadMessage()
if err != nil {
break
}
err = c.WriteMessage(mt, message)
if err != nil {
break
}
}
}))
file := suite.adaptFile(c, "fixtures/websocket/config.toml", struct {
WebsocketServer string
}{
WebsocketServer: srv.URL,
})
defer os.Remove(file)
cmd, _ := suite.cmdTraefik(withConfigFile(file), "--debug")
err := cmd.Start()
c.Assert(err, check.IsNil)
defer cmd.Process.Kill()
// wait for traefik
err = utils.TryRequest("http://127.0.0.1:8080/api/providers", 60*time.Second, func(res *http.Response) error {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
if !strings.Contains(string(body), "127.0.0.1") {
return errors.New("Incorrect traefik config")
}
return nil
})
c.Assert(err, checker.IsNil)
config, err := websocket.NewConfig("ws://127.0.0.1:8000/ws", "ws://127.0.0.1:80")
c.Assert(err, check.IsNil)
conn, err := net.DialTimeout("tcp", "127.0.0.1:8000", time.Second)
client, err := websocket.NewClient(config, conn)
c.Assert(err, checker.IsNil)
n, err := client.Write([]byte("OK"))
c.Assert(err, checker.IsNil)
c.Assert(n, checker.Equals, 2)
msg := make([]byte, 2)
n, err = client.Read(msg)
c.Assert(err, checker.IsNil)
c.Assert(n, checker.Equals, 2)
c.Assert(string(msg), checker.Equals, "OK")
}

View File

@@ -1,17 +1,25 @@
package middlewares
import (
"compress/gzip"
"net/http"
"github.com/NYTimes/gziphandler"
"github.com/containous/traefik/log"
)
// Compress is a middleware that allows redirections
type Compress struct {
// Compress is a middleware that allows redirection
type Compress struct{}
// ServerHTTP is a function used by Negroni
func (c *Compress) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
gzipHandler(next).ServeHTTP(rw, r)
}
// ServerHTTP is a function used by negroni
func (c *Compress) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
newGzipHandler := gziphandler.GzipHandler(next)
newGzipHandler.ServeHTTP(rw, r)
func gzipHandler(h http.Handler) http.Handler {
wrapper, err := gziphandler.NewGzipHandler(gzip.DefaultCompression, gziphandler.DefaultMinSize, &gziphandler.GzipResponseWriterWrapper{})
if err != nil {
log.Error(err)
}
return wrapper(h)
}

View File

@@ -0,0 +1,149 @@
package middlewares
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/NYTimes/gziphandler"
"github.com/codegangsta/negroni"
"github.com/containous/traefik/testhelpers"
"github.com/stretchr/testify/assert"
)
const (
acceptEncodingHeader = "Accept-Encoding"
contentEncodingHeader = "Content-Encoding"
varyHeader = "Vary"
gzipValue = "gzip"
)
func TestShouldCompressWhenNoContentEncodingHeader(t *testing.T) {
handler := &Compress{}
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Add(acceptEncodingHeader, gzipValue)
baseBody := generateBytes(gziphandler.DefaultMinSize)
next := func(rw http.ResponseWriter, r *http.Request) {
rw.Write(baseBody)
}
rw := httptest.NewRecorder()
handler.ServeHTTP(rw, req, next)
assert.Equal(t, gzipValue, rw.Header().Get(contentEncodingHeader))
assert.Equal(t, acceptEncodingHeader, rw.Header().Get(varyHeader))
if assert.ObjectsAreEqualValues(rw.Body.Bytes(), baseBody) {
assert.Fail(t, "expected a compressed body", "got %v", rw.Body.Bytes())
}
}
func TestShouldNotCompressWhenContentEncodingHeader(t *testing.T) {
handler := &Compress{}
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
req.Header.Add(acceptEncodingHeader, gzipValue)
fakeCompressedBody := generateBytes(gziphandler.DefaultMinSize)
next := func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add(contentEncodingHeader, gzipValue)
rw.Header().Add(varyHeader, acceptEncodingHeader)
rw.Write(fakeCompressedBody)
}
rw := httptest.NewRecorder()
handler.ServeHTTP(rw, req, next)
assert.Equal(t, gzipValue, rw.Header().Get(contentEncodingHeader))
assert.Equal(t, acceptEncodingHeader, rw.Header().Get(varyHeader))
assert.EqualValues(t, rw.Body.Bytes(), fakeCompressedBody)
}
func TestShouldNotCompressWhenNoAcceptEncodingHeader(t *testing.T) {
handler := &Compress{}
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost", nil)
fakeBody := generateBytes(gziphandler.DefaultMinSize)
next := func(rw http.ResponseWriter, r *http.Request) {
rw.Write(fakeBody)
}
rw := httptest.NewRecorder()
handler.ServeHTTP(rw, req, next)
assert.Empty(t, rw.Header().Get(contentEncodingHeader))
assert.EqualValues(t, rw.Body.Bytes(), fakeBody)
}
func TestIntegrationShouldNotCompressWhenContentAlreadyCompressed(t *testing.T) {
fakeCompressedBody := generateBytes(100000)
handler := func(rw http.ResponseWriter, r *http.Request) {
rw.Header().Add(contentEncodingHeader, gzipValue)
rw.Header().Add(varyHeader, acceptEncodingHeader)
rw.Write(fakeCompressedBody)
}
comp := &Compress{}
negro := negroni.New(comp)
negro.UseHandlerFunc(handler)
ts := httptest.NewServer(negro)
defer ts.Close()
client := &http.Client{}
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
req.Header.Add(acceptEncodingHeader, gzipValue)
resp, err := client.Do(req)
assert.NoError(t, err, "there should be no error")
assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader))
assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader))
body, err := ioutil.ReadAll(resp.Body)
assert.EqualValues(t, fakeCompressedBody, body)
}
func TestIntegrationShouldCompressWhenAcceptEncodingHeaderIsPresent(t *testing.T) {
fakeBody := generateBytes(100000)
handler := func(rw http.ResponseWriter, r *http.Request) {
rw.Write(fakeBody)
}
comp := &Compress{}
negro := negroni.New(comp)
negro.UseHandlerFunc(handler)
ts := httptest.NewServer(negro)
defer ts.Close()
client := &http.Client{}
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
req.Header.Add(acceptEncodingHeader, gzipValue)
resp, err := client.Do(req)
assert.NoError(t, err, "there should be no error")
assert.Equal(t, gzipValue, resp.Header.Get(contentEncodingHeader))
assert.Equal(t, acceptEncodingHeader, resp.Header.Get(varyHeader))
body, err := ioutil.ReadAll(resp.Body)
if assert.ObjectsAreEqualValues(body, fakeBody) {
assert.Fail(t, "expected a compressed body", "got %v", body)
}
}
func generateBytes(len int) []byte {
var value []byte
for i := 0; i < len; i++ {
value = append(value, 0x61+byte(i))
}
return value
}

View File

@@ -7,6 +7,7 @@ import (
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync/atomic"
@@ -54,6 +55,13 @@ type logInfoResponseWriter struct {
// NewLogger returns a new Logger instance.
func NewLogger(file string) *Logger {
if len(file) > 0 {
dir := filepath.Dir(file)
err := os.MkdirAll(dir, 0755)
if err != nil {
log.Errorf("Failed to create log path %s: %s", dir, err)
}
fi, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Error("Error opening file", err)

View File

@@ -7,7 +7,6 @@ import (
"net/url"
"os"
"path/filepath"
"runtime"
"testing"
shellwords "github.com/mattn/go-shellwords"
@@ -18,8 +17,7 @@ type logtestResponseWriter struct{}
var (
logger *Logger
logfileName = "traefikTestLogger.log"
logfilePath string
logfileNameSuffix = "/traefik/logger/test.log"
helloWorld = "Hello, World"
testBackendName = "http://127.0.0.1/testBackend"
testFrontendName = "testFrontend"
@@ -39,14 +37,21 @@ var (
)
func TestLogger(t *testing.T) {
if runtime.GOOS == "windows" {
logfilePath = filepath.Join(os.Getenv("TEMP"), logfileName)
} else {
logfilePath = filepath.Join("/tmp", logfileName)
tmp, err := ioutil.TempDir("", "testlogger")
if err != nil {
t.Fatalf("failed to create temp dir: %s", err)
}
defer os.RemoveAll(tmp)
logfilePath := filepath.Join(tmp, logfileNameSuffix)
logger = NewLogger(logfilePath)
defer cleanup()
defer logger.Close()
if _, err := os.Stat(logfilePath); os.IsNotExist(err) {
t.Fatalf("logger should create %s", logfilePath)
}
SetBackend2FrontendMap(&testBackend2FrontendMap)
r := &http.Request{
@@ -86,11 +91,6 @@ func TestLogger(t *testing.T) {
}
}
func cleanup() {
logger.Close()
os.Remove(logfilePath)
}
func printLogdata(logdata []byte) string {
return fmt.Sprintf(
"\nExpected: %s\n"+

33
middlewares/recover.go Normal file
View File

@@ -0,0 +1,33 @@
package middlewares
import (
"net/http"
"github.com/codegangsta/negroni"
"github.com/containous/traefik/log"
)
// RecoverHandler recovers from a panic in http handlers
func RecoverHandler(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
defer recoverFunc(w)
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
// NegroniRecoverHandler recovers from a panic in negroni handlers
func NegroniRecoverHandler() negroni.Handler {
fn := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
defer recoverFunc(w)
next.ServeHTTP(w, r)
}
return negroni.HandlerFunc(fn)
}
func recoverFunc(w http.ResponseWriter) {
if err := recover(); err != nil {
log.Errorf("Recovered from panic in http handler: %+v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}

View File

@@ -0,0 +1,45 @@
package middlewares
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/codegangsta/negroni"
)
func TestRecoverHandler(t *testing.T) {
fn := func(w http.ResponseWriter, r *http.Request) {
panic("I love panicing!")
}
recoverHandler := RecoverHandler(http.HandlerFunc(fn))
server := httptest.NewServer(recoverHandler)
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("Received non-%d response: %d\n", http.StatusInternalServerError, resp.StatusCode)
}
}
func TestNegroniRecoverHandler(t *testing.T) {
n := negroni.New()
n.Use(NegroniRecoverHandler())
panicHandler := func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
panic("I love panicing!")
}
n.UseFunc(negroni.HandlerFunc(panicHandler))
server := httptest.NewServer(n)
defer server.Close()
resp, err := http.Get(server.URL)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusInternalServerError {
t.Fatalf("Received non-%d response: %d\n", http.StatusInternalServerError, resp.StatusCode)
}
}

View File

@@ -16,5 +16,6 @@ const ReplacedPathHeader = "X-Replaced-Path"
func (s *ReplacePath) ServeHTTP(w http.ResponseWriter, r *http.Request) {
r.Header.Add(ReplacedPathHeader, r.URL.Path)
r.URL.Path = s.Path
r.RequestURI = r.URL.RequestURI()
s.Handler.ServeHTTP(w, r)
}

View File

@@ -1,10 +1,11 @@
package middlewares_test
package middlewares
import (
"net/http"
"testing"
"github.com/containous/traefik/middlewares"
"github.com/containous/traefik/testhelpers"
"github.com/stretchr/testify/assert"
)
func TestReplacePath(t *testing.T) {
@@ -17,28 +18,24 @@ func TestReplacePath(t *testing.T) {
for _, path := range paths {
t.Run(path, func(t *testing.T) {
var newPath, oldPath string
handler := &middlewares.ReplacePath{
var expectedPath, actualHeader, requestURI string
handler := &ReplacePath{
Path: replacementPath,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
newPath = r.URL.Path
oldPath = r.Header.Get("X-Replaced-Path")
expectedPath = r.URL.Path
actualHeader = r.Header.Get(ReplacedPathHeader)
requestURI = r.RequestURI
}),
}
req, err := http.NewRequest("GET", "http://localhost"+path, nil)
if err != nil {
t.Error(err)
}
req := testhelpers.MustNewRequest(http.MethodGet, "http://localhost"+path, nil)
handler.ServeHTTP(nil, req)
if newPath != replacementPath {
t.Fatalf("new path should be '%s'", replacementPath)
}
if oldPath != path {
t.Fatalf("old path should be '%s'", path)
}
assert.Equal(t, expectedPath, replacementPath, "Unexpected path.")
assert.Equal(t, path, actualHeader, "Unexpected '%s' header.", ReplacedPathHeader)
assert.Equal(t, expectedPath, requestURI, "Unexpected request URI.")
})
}
}

View File

@@ -12,10 +12,7 @@ import (
)
var (
_ http.ResponseWriter = &ResponseRecorder{}
_ http.Hijacker = &ResponseRecorder{}
_ http.Flusher = &ResponseRecorder{}
_ http.CloseNotifier = &ResponseRecorder{}
_ Stateful = &ResponseRecorder{}
)
// Retry is a middleware that retries requests

12
middlewares/stateful.go Normal file
View File

@@ -0,0 +1,12 @@
package middlewares
import "net/http"
// Stateful interface groups all http interfaces that must be
// implemented by a stateful middleware (ie: recorders)
type Stateful interface {
http.ResponseWriter
http.Hijacker
http.Flusher
http.CloseNotifier
}

View File

@@ -1,11 +1,17 @@
package middlewares
import (
"bufio"
"net"
"net/http"
"sync"
"time"
)
var (
_ Stateful = &responseRecorder{}
)
// StatsRecorder is an optional middleware that records more details statistics
// about requests and how they are processed. This currently consists of recent
// requests that have caused errors (4xx and 5xx status codes), making it easy
@@ -51,6 +57,23 @@ func (r *responseRecorder) WriteHeader(status int) {
r.statusCode = status
}
// Hijack hijacks the connection
func (r *responseRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return r.ResponseWriter.(http.Hijacker).Hijack()
}
// CloseNotify returns a channel that receives at most a
// single value (true) when the client connection has gone
// away.
func (r *responseRecorder) CloseNotify() <-chan bool {
return r.ResponseWriter.(http.CloseNotifier).CloseNotify()
}
// Flush sends any buffered data to the client.
func (r *responseRecorder) Flush() {
r.ResponseWriter.(http.Flusher).Flush()
}
// ServeHTTP silently extracts information from the request and response as it
// is processed. If the response is 4xx or 5xx, add it to the list of 10 most
// recent errors.

View File

@@ -17,17 +17,29 @@ type StripPrefix struct {
func (s *StripPrefix) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, prefix := range s.Prefixes {
if p := strings.TrimPrefix(r.URL.Path, strings.TrimSpace(prefix)); len(p) < len(r.URL.Path) {
r.URL.Path = p
r.Header[forwardedPrefixHeader] = []string{prefix}
r.RequestURI = r.URL.RequestURI()
s.Handler.ServeHTTP(w, r)
origPrefix := strings.TrimSpace(prefix)
if origPrefix == r.URL.Path {
r.URL.Path = "/"
s.serveRequest(w, r, origPrefix)
return
}
prefix = strings.TrimSuffix(origPrefix, "/") + "/"
if p := strings.TrimPrefix(r.URL.Path, prefix); len(p) < len(r.URL.Path) {
r.URL.Path = "/" + strings.TrimPrefix(p, "/")
s.serveRequest(w, r, origPrefix)
return
}
}
http.NotFound(w, r)
}
func (s *StripPrefix) serveRequest(w http.ResponseWriter, r *http.Request, prefix string) {
r.Header[forwardedPrefixHeader] = []string{prefix}
r.RequestURI = r.URL.RequestURI()
s.Handler.ServeHTTP(w, r)
}
// SetHandler sets handler
func (s *StripPrefix) SetHandler(Handler http.Handler) {
s.Handler = Handler

View File

@@ -0,0 +1,103 @@
package middlewares
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStripPrefix(t *testing.T) {
tests := []struct {
desc string
prefixes []string
path string
expectedStatusCode int
expectedPath string
}{
{
desc: "no prefixes configured",
prefixes: []string{},
path: "/noprefixes",
expectedStatusCode: http.StatusNotFound,
},
{
desc: "wildcard (.*) requests",
prefixes: []string{"/"},
path: "/",
expectedStatusCode: http.StatusOK,
expectedPath: "/",
},
{
desc: "prefix and path matching",
prefixes: []string{"/stat"},
path: "/stat",
expectedStatusCode: http.StatusOK,
expectedPath: "/",
},
{
desc: "path prefix on exactly matching path",
prefixes: []string{"/stat/"},
path: "/stat/",
expectedStatusCode: http.StatusOK,
expectedPath: "/",
},
{
desc: "path prefix on matching longer path",
prefixes: []string{"/stat/"},
path: "/stat/us",
expectedStatusCode: http.StatusOK,
expectedPath: "/us",
},
{
desc: "path prefix on mismatching path",
prefixes: []string{"/stat/"},
path: "/status",
expectedStatusCode: http.StatusNotFound,
},
{
desc: "general prefix on matching path",
prefixes: []string{"/stat"},
path: "/stat/",
expectedStatusCode: http.StatusOK,
expectedPath: "/",
},
{
desc: "earlier prefix matching",
prefixes: []string{"/stat", "/stat/us"},
path: "/stat/us",
expectedStatusCode: http.StatusOK,
expectedPath: "/us",
},
{
desc: "later prefix matching",
prefixes: []string{"/mismatch", "/stat"},
path: "/stat",
expectedStatusCode: http.StatusOK,
expectedPath: "/",
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
var gotPath string
server := httptest.NewServer(&StripPrefix{
Prefixes: test.prefixes,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
}),
})
defer server.Close()
resp, err := http.Get(server.URL + test.path)
require.NoError(t, err, "Failed to send GET request")
assert.Equal(t, test.expectedStatusCode, resp.StatusCode, "Unexpected status code")
assert.Equal(t, test.expectedPath, gotPath, "Unexpected path")
})
}
}

View File

@@ -51,6 +51,7 @@ pages:
- 'Swarm cluster': 'user-guide/swarm.md'
- 'Swarm mode cluster': 'user-guide/swarm-mode.md'
- 'Kubernetes': 'user-guide/kubernetes.md'
- 'Marathon': 'user-guide/marathon.md'
- 'Key-value store configuration': 'user-guide/kv-config.md'
- 'Clustering/HA': 'user-guide/cluster.md'
- Benchmarks: benchmarks.md

View File

@@ -25,13 +25,13 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
if err != nil {
return fmt.Errorf("Failed to Connect to KV store: %v", err)
}
p.Kvclient = store
p.SetKVClient(store)
return p.Provider.Provide(configurationChan, pool, constraints)
}
// CreateStore creates the KV store
func (p *Provider) CreateStore() (store.Store, error) {
p.StoreType = store.BOLTDB
p.SetStoreType(store.BOLTDB)
boltdb.Register()
return p.Provider.CreateStore()
}

View File

@@ -25,13 +25,13 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
if err != nil {
return fmt.Errorf("Failed to Connect to KV store: %v", err)
}
p.Kvclient = store
p.SetKVClient(store)
return p.Provider.Provide(configurationChan, pool, constraints)
}
// CreateStore creates the KV store
func (p *Provider) CreateStore() (store.Store, error) {
p.StoreType = store.CONSUL
p.SetStoreType(store.CONSUL)
consul.Register()
return p.Provider.CreateStore()
}

View File

@@ -25,13 +25,13 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
if err != nil {
return fmt.Errorf("Failed to Connect to KV store: %v", err)
}
p.Kvclient = store
p.SetKVClient(store)
return p.Provider.Provide(configurationChan, pool, constraints)
}
// CreateStore creates the KV store
func (p *Provider) CreateStore() (store.Store, error) {
p.StoreType = store.ETCD
p.SetStoreType(store.ETCD)
etcd.Register()
return p.Provider.CreateStore()
}

View File

@@ -8,9 +8,9 @@ import (
"time"
"github.com/ArthurHlt/go-eureka-client/eureka"
log "github.com/Sirupsen/logrus"
"github.com/cenk/backoff"
"github.com/containous/traefik/job"
"github.com/containous/traefik/log"
"github.com/containous/traefik/provider"
"github.com/containous/traefik/safe"
"github.com/containous/traefik/types"
@@ -19,8 +19,8 @@ import (
// Provider holds configuration of the Provider provider.
type Provider struct {
provider.BaseProvider `mapstructure:",squash"`
Endpoint string
Delay string
Endpoint string `description:"Eureka server endpoint"`
Delay string `description:"Override default configuration time between refresh"`
}
// Provide allows the eureka provider to provide configurations to traefik

View File

@@ -221,7 +221,7 @@ func (c *clientImpl) WatchSecrets(watchCh chan<- interface{}, stopCh <-chan stru
c.secStore, c.secController = cache.NewInformer(
source,
&v1.Endpoints{},
&v1.Secret{},
resyncPeriod,
newResourceEventHandlerFuncs(watchCh))
go c.secController.Run(stopCh)

View File

@@ -4,6 +4,7 @@ import (
"bufio"
"bytes"
"errors"
"flag"
"fmt"
"os"
"reflect"
@@ -27,9 +28,6 @@ var _ provider.Provider = (*Provider)(nil)
const (
annotationFrontendRuleType = "traefik.frontend.rule.type"
ruleTypePathPrefixStrip = "PathPrefixStrip"
ruleTypePathStrip = "PathStrip"
ruleTypePath = "Path"
ruleTypePathPrefix = "PathPrefix"
)
@@ -65,6 +63,12 @@ func (p *Provider) newK8sClient() (Client, error) {
// Provide allows the k8s provider to provide configurations to traefik
// using the given configuration channel.
func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
// Tell glog (used by client-go) to log into STDERR. Otherwise, we risk
// certain kinds of API errors getting logged into a directory not
// available in a `FROM scratch` Docker container, causing glog to abort
// hard with an exit code > 0.
flag.Set("logtostderr", "true")
k8sClient, err := p.newK8sClient()
if err != nil {
return err
@@ -113,11 +117,11 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
}
notify := func(err error, time time.Duration) {
log.Errorf("Provider connection error %+v, retrying in %s", err, time)
log.Errorf("Provider connection error: %s; retrying in %s", err, time)
}
err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify)
if err != nil {
log.Errorf("Cannot connect to Provider server %+v", err)
log.Errorf("Cannot connect to Provider: %s", err)
}
})
@@ -156,14 +160,16 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
PassHostHeader := p.getPassHostHeader()
passHostHeaderAnnotation := i.Annotations["traefik.frontend.passHostHeader"]
switch passHostHeaderAnnotation {
case "true":
PassHostHeader = true
case "false":
passHostHeaderAnnotation, ok := i.Annotations["traefik.frontend.passHostHeader"]
switch {
case !ok:
// No op.
case passHostHeaderAnnotation == "false":
PassHostHeader = false
case passHostHeaderAnnotation == "true":
PassHostHeader = true
default:
log.Warnf("Unknown value of %s for traefik.frontend.passHostHeader, falling back to %s", passHostHeaderAnnotation, PassHostHeader)
log.Warnf("Unknown value '%s' for traefik.frontend.passHostHeader, falling back to %s", passHostHeaderAnnotation, PassHostHeader)
}
if realm := i.Annotations["ingress.kubernetes.io/auth-realm"]; realm != "" && realm != traefikDefaultRealm {
return nil, errors.New("no realm customization supported")
@@ -172,7 +178,8 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient)
if err != nil {
return nil, err
log.Errorf("Failed to retrieve basic auth configuration for ingress %s/%s: %s", i.ObjectMeta.Namespace, i.ObjectMeta.Name, err)
continue
}
templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{
Backend: r.Host + pa.Path,
@@ -197,12 +204,8 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
}
if len(pa.Path) > 0 {
ruleType, unknown := getRuleTypeFromAnnotation(i.Annotations)
switch {
case unknown:
log.Warnf("Unknown RuleType '%s' for Ingress %s/%s, falling back to PathPrefix", ruleType, i.ObjectMeta.Namespace, i.ObjectMeta.Name)
fallthrough
case ruleType == "":
ruleType := i.Annotations[annotationFrontendRuleType]
if ruleType == "" {
ruleType = ruleTypePathPrefix
}
@@ -257,28 +260,25 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
}
if !exists {
log.Errorf("Endpoints not found for %s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
continue
log.Warnf("Endpoints not found for %s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
break
}
if len(endpoints.Subsets) == 0 {
log.Warnf("Service endpoints not found for %s/%s, falling back to Service ClusterIP", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{
URL: protocol + "://" + service.Spec.ClusterIP + ":" + strconv.Itoa(int(port.Port)),
Weight: 1,
}
} else {
for _, subset := range endpoints.Subsets {
for _, address := range subset.Addresses {
url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports))
name := url
if address.TargetRef != nil && address.TargetRef.Name != "" {
name = address.TargetRef.Name
}
templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{
URL: url,
Weight: 1,
}
log.Warnf("Endpoints not available for %s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name)
break
}
for _, subset := range endpoints.Subsets {
for _, address := range subset.Addresses {
url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports))
name := url
if address.TargetRef != nil && address.TargetRef.Name != "" {
name = address.TargetRef.Name
}
templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{
URL: url,
Weight: 1,
}
}
}
@@ -298,18 +298,15 @@ func handleBasicAuthConfig(i *v1beta1.Ingress, k8sClient Client) ([]string, erro
return nil, nil
}
if strings.ToLower(authType) != "basic" {
return nil, fmt.Errorf("unsupported auth-type: %q", authType)
return nil, fmt.Errorf("unsupported auth-type on annotation ingress.kubernetes.io/auth-type: %q", authType)
}
authSecret := i.Annotations["ingress.kubernetes.io/auth-secret"]
if authSecret == "" {
return nil, errors.New("auth-secret annotation must be set")
return nil, errors.New("auth-secret annotation ingress.kubernetes.io/auth-secret must be set")
}
basicAuthCreds, err := loadAuthCredentials(i.Namespace, authSecret, k8sClient)
if err != nil {
return nil, err
}
if len(basicAuthCreds) == 0 {
return nil, errors.New("secret file without credentials")
return nil, fmt.Errorf("failed to load auth credentials: %s", err)
}
return basicAuthCreds, nil
}
@@ -322,9 +319,9 @@ func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]stri
case !ok:
return nil, fmt.Errorf("secret %q/%q not found", namespace, secretName)
case secret == nil:
return nil, errors.New("secret data must not be nil")
return nil, fmt.Errorf("data for secret %q/%q must not be nil", namespace, secretName)
case len(secret.Data) != 1:
return nil, errors.New("secret must contain single element only")
return nil, fmt.Errorf("found %d elements for secret %q/%q, must be single element exactly", len(secret.Data), namespace, secretName)
default:
}
var firstSecret []byte
@@ -339,6 +336,10 @@ func loadAuthCredentials(namespace, secretName string, k8sClient Client) ([]stri
creds = append(creds, cred)
}
}
if len(creds) == 0 {
return nil, fmt.Errorf("secret %q/%q does not contain any credentials", namespace, secretName)
}
return creds, nil
}
@@ -390,24 +391,3 @@ func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Config
}
return configuration
}
func getRuleTypeFromAnnotation(annotations map[string]string) (ruleType string, unknown bool) {
ruleType = annotations[annotationFrontendRuleType]
for _, knownRuleType := range []string{
ruleTypePathPrefixStrip,
ruleTypePathStrip,
ruleTypePath,
ruleTypePathPrefix,
} {
if strings.ToLower(ruleType) == strings.ToLower(knownRuleType) {
return knownRuleType, false
}
}
if ruleType != "" {
// Annotation is set but does not match anything we know.
unknown = true
}
return ruleType, unknown
}

View File

@@ -5,7 +5,6 @@ import (
"errors"
"fmt"
"reflect"
"strings"
"testing"
"github.com/containous/traefik/types"
@@ -333,19 +332,24 @@ func TestRuleType(t *testing.T) {
frontendRuleType string
}{
{
desc: "implicit default",
desc: "rule type annotation missing",
ingressRuleType: "",
frontendRuleType: ruleTypePathPrefix,
},
{
desc: "unknown ingress / explicit default",
ingressRuleType: "unknown",
frontendRuleType: ruleTypePathPrefix,
desc: "Path rule type annotation set",
ingressRuleType: "Path",
frontendRuleType: "Path",
},
{
desc: "explicit ingress",
ingressRuleType: ruleTypePath,
frontendRuleType: ruleTypePath,
desc: "PathStrip rule type annotation set",
ingressRuleType: "PathStrip",
frontendRuleType: "PathStrip",
},
{
desc: "PathStripPrefix rule type annotation set",
ingressRuleType: "PathStripPrefix",
frontendRuleType: "PathStripPrefix",
},
}
@@ -1773,65 +1777,6 @@ func TestInvalidPassHostHeaderValue(t *testing.T) {
}
}
func TestGetRuleTypeFromAnnotation(t *testing.T) {
tests := []struct {
in string
wantedUnknown bool
}{
{
in: ruleTypePathPrefixStrip,
wantedUnknown: false,
},
{
in: ruleTypePathStrip,
wantedUnknown: false,
},
{
in: ruleTypePath,
wantedUnknown: false,
},
{
in: ruleTypePathPrefix,
wantedUnknown: false,
},
{
wantedUnknown: false,
},
{
in: "Unknown",
wantedUnknown: true,
},
}
for _, test := range tests {
test := test
inputs := []string{test.in, strings.ToLower(test.in)}
if inputs[0] == inputs[1] {
// Lower-casing makes no difference -- truncating to single case.
inputs = inputs[:1]
}
for _, input := range inputs {
t.Run(fmt.Sprintf("in='%s'", input), func(t *testing.T) {
t.Parallel()
annotations := map[string]string{}
if test.in != "" {
annotations[annotationFrontendRuleType] = test.in
}
gotRuleType, gotUnknown := getRuleTypeFromAnnotation(annotations)
if gotUnknown != test.wantedUnknown {
t.Errorf("got unknown '%t', wanted '%t'", gotUnknown, test.wantedUnknown)
}
if gotRuleType != test.in {
t.Errorf("got rule type '%s', wanted '%s'", gotRuleType, test.in)
}
})
}
}
}
func TestKubeAPIErrors(t *testing.T) {
ingresses := []*v1beta1.Ingress{{
ObjectMeta: v1.ObjectMeta{
@@ -1968,6 +1913,21 @@ func TestMissingResources(t *testing.T) {
},
},
},
{
Host: "missing_endpoint_subsets",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Backend: v1beta1.IngressBackend{
ServiceName: "missing_endpoint_subsets_service",
ServicePort: intstr.FromInt(80),
},
},
},
},
},
},
},
},
}}
@@ -2002,6 +1962,21 @@ func TestMissingResources(t *testing.T) {
},
},
},
{
ObjectMeta: v1.ObjectMeta{
Name: "missing_endpoint_subsets_service",
UID: "4",
Namespace: "testing",
},
Spec: v1.ServiceSpec{
ClusterIP: "10.0.0.4",
Ports: []v1.ServicePort{
{
Port: 80,
},
},
},
},
}
endpoints := []*v1.Endpoints{
{
@@ -2025,6 +2000,14 @@ func TestMissingResources(t *testing.T) {
},
},
},
{
ObjectMeta: v1.ObjectMeta{
Name: "missing_endpoint_subsets_service",
UID: "4",
Namespace: "testing",
},
Subsets: []v1.EndpointSubset{},
},
}
watchChan := make(chan interface{})
@@ -2071,6 +2054,14 @@ func TestMissingResources(t *testing.T) {
Sticky: false,
},
},
"missing_endpoint_subsets": {
Servers: map[string]types.Server{},
CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{
Method: "wrr",
Sticky: false,
},
},
},
Frontends: map[string]*types.Frontend{
"fully_working": {
@@ -2091,6 +2082,15 @@ func TestMissingResources(t *testing.T) {
},
},
},
"missing_endpoint_subsets": {
Backend: "missing_endpoint_subsets",
PassHostHeader: true,
Routes: map[string]types.Route{
"missing_endpoint_subsets": {
Rule: "Host:missing_endpoint_subsets",
},
},
},
},
}

View File

@@ -26,8 +26,8 @@ type Provider struct {
TLS *provider.ClientTLS `description:"Enable TLS support"`
Username string `description:"KV Username"`
Password string `description:"KV Password"`
StoreType store.Backend
Kvclient store.Store
storeType store.Backend
kvclient store.Store
}
// CreateStore create the K/V store
@@ -47,15 +47,25 @@ func (p *Provider) CreateStore() (store.Store, error) {
}
}
return libkv.NewStore(
p.StoreType,
p.storeType,
strings.Split(p.Endpoint, ","),
storeConfig,
)
}
// SetStoreType storeType setter
func (p *Provider) SetStoreType(storeType store.Backend) {
p.storeType = storeType
}
// SetKVClient kvclient setter
func (p *Provider) SetKVClient(kvClient store.Store) {
p.kvclient = kvClient
}
func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix string, stop chan bool) error {
operation := func() error {
events, err := p.Kvclient.WatchTree(p.Prefix, make(chan struct{}))
events, err := p.kvclient.WatchTree(p.Prefix, make(chan struct{}))
if err != nil {
return fmt.Errorf("Failed to KV WatchTree: %v", err)
}
@@ -70,7 +80,7 @@ func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix
configuration := p.loadConfig()
if configuration != nil {
configurationChan <- types.ConfigMessage{
ProviderName: string(p.StoreType),
ProviderName: string(p.storeType),
Configuration: configuration,
}
}
@@ -92,7 +102,7 @@ func (p *Provider) watchKv(configurationChan chan<- types.ConfigMessage, prefix
func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
p.Constraints = append(p.Constraints, constraints...)
operation := func() error {
if _, err := p.Kvclient.Exists("qmslkjdfmqlskdjfmqlksjazçueznbvbwzlkajzebvkwjdcqmlsfj"); err != nil {
if _, err := p.kvclient.Exists("qmslkjdfmqlskdjfmqlksjazçueznbvbwzlkajzebvkwjdcqmlsfj"); err != nil {
return fmt.Errorf("Failed to test KV store connection: %v", err)
}
if p.Watch {
@@ -105,7 +115,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
}
configuration := p.loadConfig()
configurationChan <- types.ConfigMessage{
ProviderName: string(p.StoreType),
ProviderName: string(p.storeType),
Configuration: configuration,
}
return nil
@@ -152,7 +162,7 @@ func (p *Provider) loadConfig() *types.Configuration {
func (p *Provider) list(keys ...string) []string {
joinedKeys := strings.Join(keys, "")
keysPairs, err := p.Kvclient.List(joinedKeys)
keysPairs, err := p.kvclient.List(joinedKeys)
if err != nil {
log.Debugf("Cannot get keys %s %s ", joinedKeys, err)
return nil
@@ -169,7 +179,7 @@ func (p *Provider) listServers(backend string) []string {
serverNames := p.list(backend, "/servers/")
return fun.Filter(func(serverName string) bool {
key := fmt.Sprint(serverName, "/url")
if _, err := p.Kvclient.Get(key); err != nil {
if _, err := p.kvclient.Get(key); err != nil {
if err != store.ErrKeyNotFound {
log.Errorf("Failed to retrieve value for key %s: %s", key, err)
}
@@ -181,7 +191,7 @@ func (p *Provider) listServers(backend string) []string {
func (p *Provider) get(defaultValue string, keys ...string) string {
joinedKeys := strings.Join(keys, "")
keyPair, err := p.Kvclient.Get(strings.TrimPrefix(joinedKeys, "/"))
keyPair, err := p.kvclient.Get(strings.TrimPrefix(joinedKeys, "/"))
if err != nil {
log.Debugf("Cannot get key %s %s, setting default %s", joinedKeys, err, defaultValue)
return defaultValue
@@ -194,7 +204,7 @@ func (p *Provider) get(defaultValue string, keys ...string) string {
func (p *Provider) splitGet(keys ...string) []string {
joinedKeys := strings.Join(keys, "")
keyPair, err := p.Kvclient.Get(joinedKeys)
keyPair, err := p.kvclient.Get(joinedKeys)
if err != nil {
log.Debugf("Cannot get key %s %s, setting default empty", joinedKeys, err)
return []string{}
@@ -212,7 +222,7 @@ func (p *Provider) last(key string) string {
func (p *Provider) checkConstraints(keys ...string) bool {
joinedKeys := strings.Join(keys, "")
keyPair, err := p.Kvclient.Get(joinedKeys)
keyPair, err := p.kvclient.Get(joinedKeys)
value := ""
if err == nil && keyPair != nil && keyPair.Value != nil {

View File

@@ -20,21 +20,21 @@ func TestKvList(t *testing.T) {
}{
{
provider: &Provider{
Kvclient: &Mock{},
kvclient: &Mock{},
},
keys: []string{},
expected: []string{},
},
{
provider: &Provider{
Kvclient: &Mock{},
kvclient: &Mock{},
},
keys: []string{"traefik"},
expected: []string{},
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo",
@@ -48,7 +48,7 @@ func TestKvList(t *testing.T) {
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo",
@@ -62,7 +62,7 @@ func TestKvList(t *testing.T) {
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo/baz/1",
@@ -95,7 +95,7 @@ func TestKvList(t *testing.T) {
// Error case
provider := &Provider{
Kvclient: &Mock{
kvclient: &Mock{
Error: KvError{
List: store.ErrKeyNotFound,
},
@@ -115,21 +115,21 @@ func TestKvGet(t *testing.T) {
}{
{
provider: &Provider{
Kvclient: &Mock{},
kvclient: &Mock{},
},
keys: []string{},
expected: "",
},
{
provider: &Provider{
Kvclient: &Mock{},
kvclient: &Mock{},
},
keys: []string{"traefik"},
expected: "",
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo",
@@ -143,7 +143,7 @@ func TestKvGet(t *testing.T) {
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo",
@@ -157,7 +157,7 @@ func TestKvGet(t *testing.T) {
},
{
provider: &Provider{
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "foo/baz/1",
@@ -188,7 +188,7 @@ func TestKvGet(t *testing.T) {
// Error case
provider := &Provider{
Kvclient: &Mock{
kvclient: &Mock{
Error: KvError{
Get: store.ErrKeyNotFound,
},
@@ -249,7 +249,7 @@ func TestKvWatchTree(t *testing.T) {
returnedChans := make(chan chan []*store.KVPair)
provider := &KvMock{
Provider{
Kvclient: &Mock{
kvclient: &Mock{
WatchTreeMethod: func() <-chan []*store.KVPair {
c := make(chan []*store.KVPair, 10)
returnedChans <- c
@@ -378,7 +378,7 @@ func (s *Mock) Close() {
func TestKVLoadConfig(t *testing.T) {
provider := &Provider{
Prefix: "traefik",
Kvclient: &Mock{
kvclient: &Mock{
KVPairs: []*store.KVPair{
{
Key: "traefik/frontends/frontend.with.dot",

View File

@@ -45,14 +45,14 @@ type Provider struct {
DialerTimeout flaeg.Duration `description:"Set a non-default connection timeout for Marathon"`
KeepAlive flaeg.Duration `description:"Set a non-default TCP Keep Alive time in seconds"`
ForceTaskHostname bool `description:"Force to use the task's hostname."`
Basic *Basic
Basic *Basic `description:"Enable basic authentication"`
marathonClient marathon.Marathon
}
// Basic holds basic authentication specific configurations
type Basic struct {
HTTPBasicAuthUser string
HTTPBasicPassword string
HTTPBasicAuthUser string `description:"Basic authentication User"`
HTTPBasicPassword string `description:"Basic authentication Password"`
}
type lightMarathonClient interface {
@@ -166,13 +166,13 @@ func (p *Provider) loadMarathonConfig() *types.Configuration {
applications, err := p.marathonClient.Applications(nil)
if err != nil {
log.Errorf("Failed to create a client for marathon, error: %s", err)
log.Errorf("Failed to retrieve applications from Marathon, error: %s", err)
return nil
}
tasks, err := p.marathonClient.AllTasks(&marathon.AllTasksOpts{Status: "running"})
if err != nil {
log.Errorf("Failed to create a client for marathon, error: %s", err)
log.Errorf("Failed to retrieve task from Marathon, error: %s", err)
return nil
}

View File

@@ -388,8 +388,12 @@ func parseRancherData(environments []*rancher.Environment, services []*rancher.S
Containers: []string{},
}
for key, value := range service.LaunchConfig.Labels {
rancherData.Labels[key] = value.(string)
if service.LaunchConfig == nil || service.LaunchConfig.Labels == nil {
log.Warnf("Rancher Service Labels are missing. Environment: %s, service: %s", environment.Name, service.Name)
} else {
for key, value := range service.LaunchConfig.Labels {
rancherData.Labels[key] = value.(string)
}
}
for _, container := range containers {

View File

@@ -25,13 +25,13 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
if err != nil {
return fmt.Errorf("Failed to Connect to KV store: %v", err)
}
p.Kvclient = store
p.SetKVClient(store)
return p.Provider.Provide(configurationChan, pool, constraints)
}
// CreateStore creates the KV store
func (p *Provider) CreateStore() (store.Store, error) {
p.StoreType = store.ZK
p.SetStoreType(store.ZK)
zookeeper.Register()
return p.Provider.CreateStore()
}

View File

@@ -10,7 +10,7 @@ fi
# create docker image containous/traefik
echo "Updating docker containous/traefik image..."
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
docker login -u $DOCKER_USER -p $DOCKER_PASS
docker tag containous/traefik containous/traefik:${TRAVIS_COMMIT}
docker push containous/traefik:${TRAVIS_COMMIT}
docker tag containous/traefik containous/traefik:experimental

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -e
if [ -n "$TRAVIS_TAG" ] && [ "$DOCKER_VERSION" = "1.10.3" ]; then
if [ -n "$TRAVIS_TAG" ]; then
echo "Deploying..."
else
echo "Skipping deploy"
@@ -30,7 +30,7 @@ git push -q --follow-tags -u origin master > /dev/null 2>&1
# create docker image emilevauge/traefik (compatibility)
echo "Updating docker emilevauge/traefik image..."
docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS
docker login -u $DOCKER_USER -p $DOCKER_PASS
docker tag containous/traefik emilevauge/traefik:latest
docker push emilevauge/traefik:latest
docker tag emilevauge/traefik:latest emilevauge/traefik:${VERSION}

View File

@@ -173,7 +173,7 @@ func (server *Server) startHTTPServers() {
server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration)
for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints {
serverMiddlewares := []negroni.Handler{server.loggerMiddleware, metrics}
serverMiddlewares := []negroni.Handler{middlewares.NegroniRecoverHandler(), server.loggerMiddleware, metrics}
if server.globalConfiguration.Web != nil && server.globalConfiguration.Web.Metrics != nil {
if server.globalConfiguration.Web.Metrics.Prometheus != nil {
metricsMiddleware := middlewares.NewMetricsWrapper(middlewares.NewPrometheus(newServerEntryPointName, server.globalConfiguration.Web.Metrics.Prometheus))
@@ -254,19 +254,8 @@ func (server *Server) defaultConfigurationValues(configuration *types.Configurat
if configuration == nil || configuration.Frontends == nil {
return
}
for _, frontend := range configuration.Frontends {
// default endpoints if not defined in frontends
if len(frontend.EntryPoints) == 0 {
frontend.EntryPoints = server.globalConfiguration.DefaultEntryPoints
}
}
for backendName, backend := range configuration.Backends {
_, err := types.NewLoadBalancerMethod(backend.LoadBalancer)
if err != nil {
log.Debugf("Load balancer method '%+v' for backend %s: %v. Using default wrr.", backend.LoadBalancer, backendName, err)
backend.LoadBalancer = &types.LoadBalancer{Method: "wrr"}
}
}
server.configureFrontends(configuration.Frontends)
server.configureBackends(configuration.Backends)
}
func (server *Server) listenConfigurations(stop chan bool) {
@@ -655,7 +644,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
log.Errorf("Skipping frontend %s...", frontendName)
continue frontend
}
hcOpts := parseHealthCheckOptions(rebalancer, frontend.Backend, configuration.Backends[frontend.Backend].HealthCheck, *globalConfiguration.HealthCheck)
hcOpts := parseHealthCheckOptions(rebalancer, frontend.Backend, configuration.Backends[frontend.Backend].HealthCheck, globalConfiguration.HealthCheck)
if hcOpts != nil {
log.Debugf("Setting up backend health check %s", *hcOpts)
backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts)
@@ -683,7 +672,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
continue frontend
}
}
hcOpts := parseHealthCheckOptions(rr, frontend.Backend, configuration.Backends[frontend.Backend].HealthCheck, *globalConfiguration.HealthCheck)
hcOpts := parseHealthCheckOptions(rr, frontend.Backend, configuration.Backends[frontend.Backend].HealthCheck, globalConfiguration.HealthCheck)
if hcOpts != nil {
log.Debugf("Setting up backend health check %s", *hcOpts)
backendsHealthcheck[frontend.Backend] = healthcheck.NewBackendHealthCheck(*hcOpts)
@@ -733,9 +722,10 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
}
authMiddleware, err := middlewares.NewAuthenticator(auth)
if err != nil {
log.Fatal("Error creating Auth: ", err)
log.Errorf("Error creating Auth: %s", err)
} else {
negroni.Use(authMiddleware)
}
negroni.Use(authMiddleware)
}
if configuration.Backends[frontend.Backend].CircuitBreaker != nil {
log.Debugf("Creating circuit breaker %s", configuration.Backends[frontend.Backend].CircuitBreaker.Expression)
@@ -775,7 +765,17 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
}
func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http.Handler) {
// add prefix
// path replace - This needs to always be the very last on the handler chain (first in the order in this function)
// -- Replacing Path should happen at the very end of the Modifier chain, after all the Matcher+Modifiers ran
if len(serverRoute.replacePath) > 0 {
handler = &middlewares.ReplacePath{
Path: serverRoute.replacePath,
Handler: handler,
}
}
// add prefix - This needs to always be right before ReplacePath on the chain (second in order in this function)
// -- Adding Path Prefix should happen after all *Strip Matcher+Modifiers ran, but before Replace (in case it's configured)
if len(serverRoute.addPrefix) > 0 {
handler = &middlewares.AddPrefix{
Prefix: serverRoute.addPrefix,
@@ -796,14 +796,6 @@ func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http
handler = middlewares.NewStripPrefixRegex(handler, serverRoute.stripPrefixesRegex)
}
// path replace
if len(serverRoute.replacePath) > 0 {
handler = &middlewares.ReplacePath{
Path: serverRoute.replacePath,
Handler: handler,
}
}
serverRoute.route.Handler(handler)
}
@@ -843,8 +835,8 @@ func (server *Server) buildDefaultHTTPRouter() *mux.Router {
return router
}
func parseHealthCheckOptions(lb healthcheck.LoadBalancer, backend string, hc *types.HealthCheck, hcConfig HealthCheckConfig) *healthcheck.Options {
if hc == nil || hc.Path == "" {
func parseHealthCheckOptions(lb healthcheck.LoadBalancer, backend string, hc *types.HealthCheck, hcConfig *HealthCheckConfig) *healthcheck.Options {
if hc == nil || hc.Path == "" || hcConfig == nil {
return nil
}
@@ -887,3 +879,29 @@ func sortedFrontendNamesForConfig(configuration *types.Configuration) []string {
sort.Strings(keys)
return keys
}
func (server *Server) configureFrontends(frontends map[string]*types.Frontend) {
for _, frontend := range frontends {
// default endpoints if not defined in frontends
if len(frontend.EntryPoints) == 0 {
frontend.EntryPoints = server.globalConfiguration.DefaultEntryPoints
}
}
}
func (*Server) configureBackends(backends map[string]*types.Backend) {
for backendName, backend := range backends {
_, err := types.NewLoadBalancerMethod(backend.LoadBalancer)
if err != nil {
log.Debugf("Validation of load balancer method for backend %s failed: %s. Using default method wrr.", backendName, err)
var sticky bool
if backend.LoadBalancer != nil {
sticky = backend.LoadBalancer.Sticky
}
backend.LoadBalancer = &types.LoadBalancer{
Method: "wrr",
Sticky: sticky,
}
}
}
}

View File

@@ -2,14 +2,18 @@ package server
import (
"fmt"
"net/http"
"net/url"
"reflect"
"testing"
"time"
"github.com/containous/flaeg"
"github.com/containous/mux"
"github.com/containous/traefik/healthcheck"
"github.com/containous/traefik/testhelpers"
"github.com/containous/traefik/types"
"github.com/davecgh/go-spew/spew"
"github.com/vulcand/oxy/roundrobin"
)
@@ -27,6 +31,85 @@ func (lb *testLoadBalancer) Servers() []*url.URL {
return []*url.URL{}
}
func TestServerMultipleFrontendRules(t *testing.T) {
cases := []struct {
expression string
requestURL string
expectedURL string
}{
{
expression: "Host:foo.bar",
requestURL: "http://foo.bar",
expectedURL: "http://foo.bar",
},
{
expression: "PathPrefix:/management;ReplacePath:/health",
requestURL: "http://foo.bar/management",
expectedURL: "http://foo.bar/health",
},
{
expression: "Host:foo.bar;AddPrefix:/blah",
requestURL: "http://foo.bar/baz",
expectedURL: "http://foo.bar/blah/baz",
},
{
expression: "PathPrefixStripRegex:/one/{two}/{three:[0-9]+}",
requestURL: "http://foo.bar/one/some/12345/four",
expectedURL: "http://foo.bar/four",
},
{
expression: "PathPrefixStripRegex:/one/{two}/{three:[0-9]+};AddPrefix:/zero",
requestURL: "http://foo.bar/one/some/12345/four",
expectedURL: "http://foo.bar/zero/four",
},
{
expression: "AddPrefix:/blah;ReplacePath:/baz",
requestURL: "http://foo.bar/hello",
expectedURL: "http://foo.bar/baz",
},
{
expression: "PathPrefixStrip:/management;ReplacePath:/health",
requestURL: "http://foo.bar/management",
expectedURL: "http://foo.bar/health",
},
}
for _, test := range cases {
test := test
t.Run(test.expression, func(t *testing.T) {
t.Parallel()
router := mux.NewRouter()
route := router.NewRoute()
serverRoute := &serverRoute{route: route}
rules := &Rules{route: serverRoute}
expression := test.expression
routeResult, err := rules.Parse(expression)
if err != nil {
t.Fatalf("Error while building route for %s: %+v", expression, err)
}
request := testhelpers.MustNewRequest(http.MethodGet, test.requestURL, nil)
routeMatch := routeResult.Match(request, &mux.RouteMatch{Route: routeResult})
if !routeMatch {
t.Fatalf("Rule %s doesn't match", expression)
}
server := new(Server)
server.wireFrontendBackend(serverRoute, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.String() != test.expectedURL {
t.Fatalf("got URL %s, expected %s", r.URL.String(), test.expectedURL)
}
}))
serverRoute.route.GetHandler().ServeHTTP(nil, request)
})
}
}
func TestServerLoadConfigHealthCheckOptions(t *testing.T) {
healthChecks := []*types.HealthCheck{
nil,
@@ -151,10 +234,125 @@ func TestServerParseHealthCheckOptions(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
gotOpts := parseHealthCheckOptions(lb, "backend", test.hc, HealthCheckConfig{Interval: flaeg.Duration(globalInterval)})
gotOpts := parseHealthCheckOptions(lb, "backend", test.hc, &HealthCheckConfig{Interval: flaeg.Duration(globalInterval)})
if !reflect.DeepEqual(gotOpts, test.wantOpts) {
t.Errorf("got health check options %+v, want %+v", gotOpts, test.wantOpts)
}
})
}
}
func TestServerLoadConfigEmptyBasicAuth(t *testing.T) {
globalConfig := GlobalConfiguration{
EntryPoints: EntryPoints{
"http": &EntryPoint{},
},
}
dynamicConfigs := configs{
"config": &types.Configuration{
Frontends: map[string]*types.Frontend{
"frontend": {
EntryPoints: []string{"http"},
Backend: "backend",
BasicAuth: []string{""},
},
},
Backends: map[string]*types.Backend{
"backend": {
Servers: map[string]types.Server{
"server": {
URL: "http://localhost",
},
},
LoadBalancer: &types.LoadBalancer{
Method: "Wrr",
},
},
},
},
}
srv := NewServer(globalConfig)
if _, err := srv.loadConfig(dynamicConfigs, globalConfig); err != nil {
t.Fatalf("got error: %s", err)
}
}
func TestConfigureBackends(t *testing.T) {
validMethod := "Drr"
defaultMethod := "wrr"
tests := []struct {
desc string
lb *types.LoadBalancer
wantMethod string
wantSticky bool
}{
{
desc: "valid load balancer method with sticky enabled",
lb: &types.LoadBalancer{
Method: validMethod,
Sticky: true,
},
wantMethod: validMethod,
wantSticky: true,
},
{
desc: "valid load balancer method with sticky disabled",
lb: &types.LoadBalancer{
Method: validMethod,
Sticky: false,
},
wantMethod: validMethod,
wantSticky: false,
},
{
desc: "invalid load balancer method with sticky enabled",
lb: &types.LoadBalancer{
Method: "Invalid",
Sticky: true,
},
wantMethod: defaultMethod,
wantSticky: true,
},
{
desc: "invalid load balancer method with sticky disabled",
lb: &types.LoadBalancer{
Method: "Invalid",
Sticky: false,
},
wantMethod: defaultMethod,
wantSticky: false,
},
{
desc: "missing load balancer",
lb: nil,
wantMethod: defaultMethod,
wantSticky: false,
},
}
for _, test := range tests {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
backend := &types.Backend{
LoadBalancer: test.lb,
}
srv := Server{}
srv.configureBackends(map[string]*types.Backend{
"backend": backend,
})
wantLB := types.LoadBalancer{
Method: test.wantMethod,
Sticky: test.wantSticky,
}
if !reflect.DeepEqual(*backend.LoadBalancer, wantLB) {
t.Errorf("got backend load-balancer\n%v\nwant\n%v\n", spew.Sdump(backend.LoadBalancer), spew.Sdump(wantLB))
}
})
}
}

View File

@@ -1,4 +1,5 @@
[backends]{{range $backendName, $backend := .Backends}}
[backends."{{$backendName}}"]
{{if $backend.CircuitBreaker}}
[backends."{{$backendName}}".circuitbreaker]
expression = "{{$backend.CircuitBreaker.Expression}}"

View File

@@ -1,6 +1,21 @@
package testhelpers
import (
"fmt"
"io"
"net/http"
)
// Intp returns a pointer to the given integer value.
func Intp(i int) *int {
return &i
}
// MustNewRequest creates a new http get request or panics if it can't
func MustNewRequest(method, urlStr string, body io.Reader) *http.Request {
request, err := http.NewRequest(method, urlStr, body)
if err != nil {
panic(fmt.Sprintf("failed to create HTTP %s Request for '%s': %s", method, urlStr, err))
}
return request
}

View File

@@ -81,19 +81,18 @@ var loadBalancerMethodNames = []string{
// NewLoadBalancerMethod create a new LoadBalancerMethod from a given LoadBalancer.
func NewLoadBalancerMethod(loadBalancer *LoadBalancer) (LoadBalancerMethod, error) {
var method string
if loadBalancer != nil {
method = loadBalancer.Method
for i, name := range loadBalancerMethodNames {
if strings.EqualFold(name, loadBalancer.Method) {
if strings.EqualFold(name, method) {
return LoadBalancerMethod(i), nil
}
}
}
return Wrr, ErrInvalidLoadBalancerMethod
return Wrr, fmt.Errorf("invalid load-balancing method '%s'", method)
}
// ErrInvalidLoadBalancerMethod is thrown when the specified load balancing method is invalid.
var ErrInvalidLoadBalancerMethod = errors.New("Invalid method, using default")
// Configuration of a provider.
type Configuration struct {
Backends map[string]*Backend `json:"backends,omitempty"`

View File

@@ -3,6 +3,7 @@ package gziphandler
import (
"bufio"
"compress/gzip"
"errors"
"fmt"
"io"
"net"
@@ -97,6 +98,7 @@ func (w *GzipResponseWriter) Write(b []byte) (int, error) {
}
// Save the write into a buffer for later use in GZIP responseWriter (if content is long enough) or at close with regular responseWriter.
// On the first write, w.buf changes from nil to a valid slice
w.buf = append(w.buf, b...)
// If the global writes are bigger than the minSize, compression is enable.
@@ -122,7 +124,9 @@ func (w *GzipResponseWriter) startGzip() error {
w.Header().Del(contentLength)
// Write the header to gzip response.
w.writeHeader()
if w.code != 0 {
w.ResponseWriter.WriteHeader(w.code)
}
// Initialize the GZIP response.
w.init()
@@ -146,14 +150,6 @@ func (w *GzipResponseWriter) WriteHeader(code int) {
w.code = code
}
// writeHeader uses the saved code to send it to the ResponseWriter.
func (w *GzipResponseWriter) writeHeader() {
if w.code == 0 {
w.code = http.StatusOK
}
w.ResponseWriter.WriteHeader(w.code)
}
// init graps a new gzip writer from the gzipWriterPool and writes the correct
// content encoding header.
func (w *GzipResponseWriter) init() {
@@ -166,19 +162,18 @@ func (w *GzipResponseWriter) init() {
// Close will close the gzip.Writer and will put it back in the gzipWriterPool.
func (w *GzipResponseWriter) Close() error {
// Buffer not nil means the regular response must be returned.
if w.buf != nil {
w.writeHeader()
// Make the write into the regular response.
_, writeErr := w.ResponseWriter.Write(w.buf)
// Returns the error if any at write.
if writeErr != nil {
return fmt.Errorf("gziphandler: write to regular responseWriter at close gets error: %q", writeErr.Error())
}
}
// If the GZIP responseWriter is not set no needs to close it.
if w.gw == nil {
// Gzip not trigged yet, write out regular response.
if w.code != 0 {
w.ResponseWriter.WriteHeader(w.code)
}
if w.buf != nil {
_, writeErr := w.ResponseWriter.Write(w.buf)
// Returns the error if any at write.
if writeErr != nil {
return fmt.Errorf("gziphandler: write to regular responseWriter at close gets error: %q", writeErr.Error())
}
}
return nil
}
@@ -236,12 +231,22 @@ func NewGzipLevelHandler(level int) (func(http.Handler) http.Handler, error) {
// NewGzipLevelAndMinSize behave as NewGzipLevelHandler except it let the caller
// specify the minimum size before compression.
func NewGzipLevelAndMinSize(level, minSize int) (func(http.Handler) http.Handler, error) {
return NewGzipHandler(level, minSize, &GzipResponseWriter{})
}
// NewGzipHandler behave as NewGzipLevelHandler except it let the caller
// specify the minimum size before compression and a GzipWriter.
func NewGzipHandler(level, minSize int, gw GzipWriter) (func(http.Handler) http.Handler, error) {
if level != gzip.DefaultCompression && (level < gzip.BestSpeed || level > gzip.BestCompression) {
return nil, fmt.Errorf("invalid compression level requested: %d", level)
}
if minSize < 0 {
return nil, fmt.Errorf("minimum size must be more than zero")
return nil, errors.New("minimum size must be more than zero")
}
if gw == nil {
return nil, errors.New("the GzipWriter must be defined")
}
return func(h http.Handler) http.Handler {
index := poolIndex(level)
@@ -249,13 +254,9 @@ func NewGzipLevelAndMinSize(level, minSize int) (func(http.Handler) http.Handler
w.Header().Add(vary, acceptEncoding)
if acceptsGzip(r) {
gw := &GzipResponseWriter{
ResponseWriter: w,
index: index,
minSize: minSize,
buf: []byte{},
}
gw.SetResponseWriter(w)
gw.setIndex(index)
gw.setMinSize(minSize)
defer gw.Close()
h.ServeHTTP(gw, r)

58
vendor/github.com/NYTimes/gziphandler/wrapper.go generated vendored Normal file
View File

@@ -0,0 +1,58 @@
package gziphandler
import (
"bufio"
"net"
"net/http"
)
const (
contentEncodingHeader = "Content-Encoding"
)
// ----------
// http.ResponseWriter
// http.Hijacker
type GzipWriter interface {
Header() http.Header
Write([]byte) (int, error)
WriteHeader(int)
Hijack() (net.Conn, *bufio.ReadWriter, error)
Close() error
SetResponseWriter(http.ResponseWriter)
setIndex(int)
setMinSize(int)
}
func (w *GzipResponseWriter) SetResponseWriter(rw http.ResponseWriter) {
w.ResponseWriter = rw
}
func (w *GzipResponseWriter) setIndex(index int) {
w.index = index
}
func (w *GzipResponseWriter) setMinSize(minSize int) {
w.minSize = minSize
}
// --------
type GzipResponseWriterWrapper struct {
GzipResponseWriter
}
func (g *GzipResponseWriterWrapper) Write(b []byte) (int, error) {
if g.gw == nil && isEncoded(g.Header()) {
return g.ResponseWriter.Write(b)
}
return g.GzipResponseWriter.Write(b)
}
func isEncoded(headers http.Header) bool {
header := headers.Get(contentEncodingHeader)
// According to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding,
// content is not encoded if the header 'Content-Encoding' is empty or equals to 'identity'.
return header != "" && header != "identity"
}

View File

@@ -4,6 +4,7 @@ import (
"log"
"net/http"
"strings"
"sync"
)
type subscription struct {
@@ -32,6 +33,8 @@ type Server struct {
subs chan *subscription
unregister chan *subscription
quit chan bool
isClosed bool
isClosedMutex sync.RWMutex
}
// Create a new Server ready for handler creation and publishing events
@@ -51,6 +54,7 @@ func NewServer() *Server {
// Stop handling publishing
func (srv *Server) Close() {
srv.quit <- true
srv.markServerClosed()
}
// Create a new handler for serving a specified channel
@@ -69,6 +73,12 @@ func (srv *Server) Handler(channel string) http.HandlerFunc {
}
w.WriteHeader(http.StatusOK)
// If the Handler is still active even though the server is closed, stop here.
// Otherwise the Handler will block while publishing to srv.subs indefinitely.
if srv.isServerClosed() {
return
}
sub := &subscription{
channel: channel,
lastEventId: req.Header.Get("Last-Event-ID"),
@@ -165,3 +175,15 @@ func (srv *Server) run() {
}
}
}
func (srv *Server) isServerClosed() bool {
srv.isClosedMutex.RLock()
defer srv.isClosedMutex.RUnlock()
return srv.isClosed
}
func (srv *Server) markServerClosed() {
srv.isClosedMutex.Lock()
defer srv.isClosedMutex.Unlock()
srv.isClosed = true
}

View File

@@ -7,6 +7,7 @@ import (
"io/ioutil"
"log"
"net/http"
"sync"
"time"
)
@@ -27,6 +28,10 @@ type Stream struct {
Errors chan error
// Logger is a logger that, when set, will be used for logging debug messages
Logger *log.Logger
// isClosed is a marker that the stream is/should be closed
isClosed bool
// isClosedMutex is a mutex protecting concurrent read/write access of isClosed
isClosedMutex sync.RWMutex
}
type SubscriptionError struct {
@@ -61,7 +66,7 @@ func SubscribeWith(lastEventId string, client *http.Client, request *http.Reques
c: client,
req: request,
lastEventId: lastEventId,
retry: (time.Millisecond * 3000),
retry: time.Millisecond * 3000,
Events: make(chan Event),
Errors: make(chan error),
}
@@ -75,6 +80,29 @@ func SubscribeWith(lastEventId string, client *http.Client, request *http.Reques
return stream, nil
}
// Close will close the stream. It is safe for concurrent access and can be called multiple times.
func (stream *Stream) Close() {
if stream.isStreamClosed() {
return
}
stream.markStreamClosed()
close(stream.Errors)
close(stream.Events)
}
func (stream *Stream) isStreamClosed() bool {
stream.isClosedMutex.RLock()
defer stream.isClosedMutex.RUnlock()
return stream.isClosed
}
func (stream *Stream) markStreamClosed() {
stream.isClosedMutex.Lock()
defer stream.isClosedMutex.Unlock()
stream.isClosed = true
}
// Go's http package doesn't copy headers across when it encounters
// redirects so we need to do that manually.
func checkRedirect(req *http.Request, via []*http.Request) error {
@@ -112,15 +140,27 @@ func (stream *Stream) connect() (r io.ReadCloser, err error) {
func (stream *Stream) stream(r io.ReadCloser) {
defer r.Close()
// receives events until an error is encountered
stream.receiveEvents(r)
// tries to reconnect and start the stream again
stream.retryRestartStream()
}
func (stream *Stream) receiveEvents(r io.ReadCloser) {
dec := NewDecoder(r)
for {
ev, err := dec.Decode()
if stream.isStreamClosed() {
return
}
if err != nil {
stream.Errors <- err
// respond to all errors by reconnecting and trying again
break
return
}
pub := ev.(*publication)
if pub.Retry() > 0 {
stream.retry = time.Duration(pub.Retry()) * time.Millisecond
@@ -130,20 +170,25 @@ func (stream *Stream) stream(r io.ReadCloser) {
}
stream.Events <- ev
}
}
func (stream *Stream) retryRestartStream() {
backoff := stream.retry
for {
time.Sleep(backoff)
if stream.Logger != nil {
stream.Logger.Printf("Reconnecting in %0.4f secs\n", backoff.Seconds())
}
time.Sleep(backoff)
if stream.isStreamClosed() {
return
}
// NOTE: because of the defer we're opening the new connection
// before closing the old one. Shouldn't be a problem in practice,
// but something to be aware of.
next, err := stream.connect()
r, err := stream.connect()
if err == nil {
go stream.stream(next)
break
go stream.stream(r)
return
}
stream.Errors <- err
backoff *= 2

View File

@@ -89,6 +89,8 @@ type Application struct {
TaskStats map[string]TaskStats `json:"taskStats,omitempty"`
User string `json:"user,omitempty"`
UpgradeStrategy *UpgradeStrategy `json:"upgradeStrategy,omitempty"`
UnreachableStrategy *UnreachableStrategy `json:"unreachableStrategy,omitempty"`
KillSelection string `json:"killSelection,omitempty"`
Uris *[]string `json:"uris,omitempty"`
Version string `json:"version,omitempty"`
VersionInfo *VersionInfo `json:"versionInfo,omitempty"`
@@ -453,7 +455,7 @@ func (r *Application) DeploymentIDs() []*DeploymentID {
// CheckHTTP adds a HTTP check to an application
// port: the port the check should be checking
// interval: the interval in seconds the check should be performed
func (r *Application) CheckHTTP(uri string, port, interval int) (*Application, error) {
func (r *Application) CheckHTTP(path string, port, interval int) (*Application, error) {
if r.Container == nil || r.Container.Docker == nil {
return nil, ErrNoApplicationContainer
}
@@ -464,7 +466,7 @@ func (r *Application) CheckHTTP(uri string, port, interval int) (*Application, e
}
health := NewDefaultHealthCheck()
health.IntervalSeconds = interval
*health.Path = uri
*health.Path = path
*health.PortIndex = portIndex
// step: add to the checks
r.AddHealthCheck(*health)
@@ -555,6 +557,20 @@ func (r *Application) EmptyUpgradeStrategy() *Application {
return r
}
// SetUnreachableStrategy sets the unreachable strategy.
func (r *Application) SetUnreachableStrategy(us UnreachableStrategy) *Application {
r.UnreachableStrategy = &us
return r
}
// EmptyUnreachableStrategy explicitly empties the unreachable strategy -- use this if
// you need to empty the unreachable strategy of an application that already has
// the unreachable strategy set (setting it to nil will keep the current value).
func (r *Application) EmptyUnreachableStrategy() *Application {
r.UnreachableStrategy = &UnreachableStrategy{}
return r
}
// String returns the json representation of this application
func (r *Application) String() string {
s, err := json.MarshalIndent(r, "", " ")
@@ -611,9 +627,9 @@ func (r *marathonClient) HasApplicationVersion(name, version string) (bool, erro
// ApplicationVersions is a list of versions which has been deployed with marathon for a specific application
// name: the id used to identify the application
func (r *marathonClient) ApplicationVersions(name string) (*ApplicationVersions, error) {
uri := fmt.Sprintf("%s/versions", buildURI(name))
path := fmt.Sprintf("%s/versions", buildPath(name))
versions := new(ApplicationVersions)
if err := r.apiGet(uri, nil, versions); err != nil {
if err := r.apiGet(path, nil, versions); err != nil {
return nil, err
}
return versions, nil
@@ -623,9 +639,9 @@ func (r *marathonClient) ApplicationVersions(name string) (*ApplicationVersions,
// name: the id used to identify the application
// version: the version (normally a timestamp) you wish to change to
func (r *marathonClient) SetApplicationVersion(name string, version *ApplicationVersion) (*DeploymentID, error) {
uri := fmt.Sprintf(buildURI(name))
path := fmt.Sprintf(buildPath(name))
deploymentID := new(DeploymentID)
if err := r.apiPut(uri, version, deploymentID); err != nil {
if err := r.apiPut(path, version, deploymentID); err != nil {
return nil, err
}
@@ -639,7 +655,7 @@ func (r *marathonClient) Application(name string) (*Application, error) {
Application *Application `json:"app"`
}
if err := r.apiGet(buildURI(name), nil, &wrapper); err != nil {
if err := r.apiGet(buildPath(name), nil, &wrapper); err != nil {
return nil, err
}
@@ -650,7 +666,7 @@ func (r *marathonClient) Application(name string) (*Application, error) {
// name: the id used to identify the application
// opts: GetAppOpts request payload
func (r *marathonClient) ApplicationBy(name string, opts *GetAppOpts) (*Application, error) {
u, err := addOptions(buildURI(name), opts)
path, err := addOptions(buildPath(name), opts)
if err != nil {
return nil, err
}
@@ -658,7 +674,7 @@ func (r *marathonClient) ApplicationBy(name string, opts *GetAppOpts) (*Applicat
Application *Application `json:"app"`
}
if err := r.apiGet(u, nil, &wrapper); err != nil {
if err := r.apiGet(path, nil, &wrapper); err != nil {
return nil, err
}
@@ -671,8 +687,8 @@ func (r *marathonClient) ApplicationBy(name string, opts *GetAppOpts) (*Applicat
func (r *marathonClient) ApplicationByVersion(name, version string) (*Application, error) {
app := new(Application)
uri := fmt.Sprintf("%s/versions/%s", buildURI(name), version)
if err := r.apiGet(uri, nil, app); err != nil {
path := fmt.Sprintf("%s/versions/%s", buildPath(name), version)
if err := r.apiGet(path, nil, app); err != nil {
return nil, err
}
@@ -779,10 +795,10 @@ func (r *marathonClient) appExistAndRunning(name string) bool {
// name: the id used to identify the application
// force: used to force the delete operation in case of blocked deployment
func (r *marathonClient) DeleteApplication(name string, force bool) (*DeploymentID, error) {
uri := buildURIWithForceParam(name, force)
path := buildPathWithForceParam(name, force)
// step: check of the application already exists
deployID := new(DeploymentID)
if err := r.apiDelete(uri, nil, deployID); err != nil {
if err := r.apiDelete(path, nil, deployID); err != nil {
return nil, err
}
@@ -794,8 +810,8 @@ func (r *marathonClient) DeleteApplication(name string, force bool) (*Deployment
func (r *marathonClient) RestartApplication(name string, force bool) (*DeploymentID, error) {
deployment := new(DeploymentID)
var options struct{}
uri := buildURIWithForceParam(fmt.Sprintf("%s/restart", name), force)
if err := r.apiPost(uri, &options, deployment); err != nil {
path := buildPathWithForceParam(fmt.Sprintf("%s/restart", name), force)
if err := r.apiPost(path, &options, deployment); err != nil {
return nil, err
}
@@ -810,9 +826,9 @@ func (r *marathonClient) ScaleApplicationInstances(name string, instances int, f
changes := new(Application)
changes.ID = validateID(name)
changes.Instances = &instances
uri := buildURIWithForceParam(name, force)
path := buildPathWithForceParam(name, force)
deployID := new(DeploymentID)
if err := r.apiPut(uri, changes, deployID); err != nil {
if err := r.apiPut(path, changes, deployID); err != nil {
return nil, err
}
@@ -823,22 +839,22 @@ func (r *marathonClient) ScaleApplicationInstances(name string, instances int, f
// application: the structure holding the application configuration
func (r *marathonClient) UpdateApplication(application *Application, force bool) (*DeploymentID, error) {
result := new(DeploymentID)
uri := buildURIWithForceParam(application.ID, force)
if err := r.apiPut(uri, application, result); err != nil {
path := buildPathWithForceParam(application.ID, force)
if err := r.apiPut(path, application, result); err != nil {
return nil, err
}
return result, nil
}
func buildURIWithForceParam(path string, force bool) string {
uri := buildURI(path)
func buildPathWithForceParam(rootPath string, force bool) string {
path := buildPath(rootPath)
if force {
uri += "?force=true"
path += "?force=true"
}
return uri
return path
}
func buildURI(path string) string {
func buildPath(path string) string {
return fmt.Sprintf("%s/%s", marathonAPIApps, trimRootPath(path))
}

View File

@@ -27,6 +27,7 @@ import (
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"time"
)
@@ -149,8 +150,6 @@ type Marathon interface {
}
var (
// ErrInvalidResponse is thrown when marathon responds with invalid or error response
ErrInvalidResponse = errors.New("invalid response from Marathon")
// ErrMarathonDown is thrown when all the marathon endpoints are down
ErrMarathonDown = errors.New("all the Marathon hosts are presently down")
// ErrTimeoutError is thrown when the operation has timed out
@@ -189,6 +188,11 @@ type httpClient struct {
config Config
}
// newRequestError signals that creating a new http.Request failed
type newRequestError struct {
error
}
// NewClient creates a new marathon client
// config: the configuration to use
func NewClient(config Config) (Marathon, error) {
@@ -238,23 +242,23 @@ func (r *marathonClient) Ping() (bool, error) {
return true, nil
}
func (r *marathonClient) apiGet(uri string, post, result interface{}) error {
return r.apiCall("GET", uri, post, result)
func (r *marathonClient) apiGet(path string, post, result interface{}) error {
return r.apiCall("GET", path, post, result)
}
func (r *marathonClient) apiPut(uri string, post, result interface{}) error {
return r.apiCall("PUT", uri, post, result)
func (r *marathonClient) apiPut(path string, post, result interface{}) error {
return r.apiCall("PUT", path, post, result)
}
func (r *marathonClient) apiPost(uri string, post, result interface{}) error {
return r.apiCall("POST", uri, post, result)
func (r *marathonClient) apiPost(path string, post, result interface{}) error {
return r.apiCall("POST", path, post, result)
}
func (r *marathonClient) apiDelete(uri string, post, result interface{}) error {
return r.apiCall("DELETE", uri, post, result)
func (r *marathonClient) apiDelete(path string, post, result interface{}) error {
return r.apiCall("DELETE", path, post, result)
}
func (r *marathonClient) apiCall(method, url string, body, result interface{}) error {
func (r *marathonClient) apiCall(method, path string, body, result interface{}) error {
for {
// step: marshall the request to json
var requestBody []byte
@@ -266,7 +270,7 @@ func (r *marathonClient) apiCall(method, url string, body, result interface{}) e
}
// step: create the API request
request, member, err := r.buildAPIRequest(method, url, bytes.NewReader(requestBody))
request, member, err := r.buildAPIRequest(method, path, bytes.NewReader(requestBody))
if err != nil {
return err
}
@@ -297,8 +301,7 @@ func (r *marathonClient) apiCall(method, url string, body, result interface{}) e
if response.StatusCode >= 200 && response.StatusCode <= 299 {
if result != nil {
if err := json.Unmarshal(respBody, result); err != nil {
r.debugLog.Printf("apiCall(): failed to unmarshall the response from marathon, error: %s\n", err)
return ErrInvalidResponse
return fmt.Errorf("failed to unmarshal response from Marathon: %s", err)
}
}
return nil
@@ -316,8 +319,9 @@ func (r *marathonClient) apiCall(method, url string, body, result interface{}) e
}
}
// buildAPIRequest creates a default API request
func (r *marathonClient) buildAPIRequest(method, uri string, reader io.Reader) (request *http.Request, member string, err error) {
// buildAPIRequest creates a default API request.
// It fails when there is no available member in the cluster anymore or when the request can not be built.
func (r *marathonClient) buildAPIRequest(method, path string, reader io.Reader) (request *http.Request, member string, err error) {
// Grab a member from the cluster
member, err = r.hosts.getMember()
if err != nil {
@@ -325,16 +329,22 @@ func (r *marathonClient) buildAPIRequest(method, uri string, reader io.Reader) (
}
// Build the HTTP request to Marathon
request, err = r.client.buildMarathonRequest(method, member, uri, reader)
request, err = r.client.buildMarathonRequest(method, member, path, reader)
if err != nil {
return nil, member, err
return nil, member, newRequestError{err}
}
return request, member, nil
}
func (rc *httpClient) buildMarathonRequest(method string, member string, uri string, reader io.Reader) (request *http.Request, err error) {
// buildMarathonRequest creates a new HTTP request and configures it according to the *httpClient configuration.
// The path must not contain a leading "/", otherwise buildMarathonRequest will panic.
func (rc *httpClient) buildMarathonRequest(method string, member string, path string, reader io.Reader) (request *http.Request, err error) {
if strings.HasPrefix(path, "/") {
panic(fmt.Sprintf("Path '%s' must not start with a leading slash", path))
}
// Create the endpoint URL
url := fmt.Sprintf("%s/%s", member, uri)
url := fmt.Sprintf("%s/%s", member, path)
// Instantiate an HTTP request
request, err = http.NewRequest(method, url, reader)

View File

@@ -131,7 +131,7 @@ func (c *cluster) markDown(endpoint string) {
func (c *cluster) healthCheckNode(node *member) {
// step: wait for the node to become active ... we are assuming a /ping is enough here
for {
req, err := c.client.buildMarathonRequest("GET", node.endpoint, "/ping", nil)
req, err := c.client.buildMarathonRequest("GET", node.endpoint, "ping", nil)
if err == nil {
res, err := c.client.Do(req)
if err == nil && res.StatusCode == 200 {

View File

@@ -106,12 +106,12 @@ func (r *marathonClient) Group(name string) (*Group, error) {
// GroupsBy retrieves a list of all the groups from marathon by embed options
// opts: GetGroupOpts request payload
func (r *marathonClient) GroupsBy(opts *GetGroupOpts) (*Groups, error) {
u, err := addOptions(marathonAPIGroups, opts)
path, err := addOptions(marathonAPIGroups, opts)
if err != nil {
return nil, err
}
groups := new(Groups)
if err := r.apiGet(u, "", groups); err != nil {
if err := r.apiGet(path, "", groups); err != nil {
return nil, err
}
return groups, nil
@@ -121,12 +121,12 @@ func (r *marathonClient) GroupsBy(opts *GetGroupOpts) (*Groups, error) {
// name: the identifier for the group
// opts: GetGroupOpts request payload
func (r *marathonClient) GroupBy(name string, opts *GetGroupOpts) (*Group, error) {
u, err := addOptions(fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)), opts)
path, err := addOptions(fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name)), opts)
if err != nil {
return nil, err
}
group := new(Group)
if err := r.apiGet(u, nil, group); err != nil {
if err := r.apiGet(path, nil, group); err != nil {
return nil, err
}
return group, nil
@@ -135,8 +135,8 @@ func (r *marathonClient) GroupBy(name string, opts *GetGroupOpts) (*Group, error
// HasGroup checks if the group exists in marathon
// name: the identifier for the group
func (r *marathonClient) HasGroup(name string) (bool, error) {
uri := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
err := r.apiCall("GET", uri, "", nil)
path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
err := r.apiCall("GET", path, "", nil)
if err != nil {
if apiErr, ok := err.(*APIError); ok && apiErr.ErrCode == ErrCodeNotFound {
return false, nil
@@ -208,11 +208,11 @@ func (r *marathonClient) WaitOnGroup(name string, timeout time.Duration) error {
// force: used to force the delete operation in case of blocked deployment
func (r *marathonClient) DeleteGroup(name string, force bool) (*DeploymentID, error) {
version := new(DeploymentID)
uri := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
if force {
uri = uri + "?force=true"
path += "?force=true"
}
if err := r.apiDelete(uri, nil, version); err != nil {
if err := r.apiDelete(path, nil, version); err != nil {
return nil, err
}
@@ -225,11 +225,11 @@ func (r *marathonClient) DeleteGroup(name string, force bool) (*DeploymentID, er
// force: used to force the update operation in case of blocked deployment
func (r *marathonClient) UpdateGroup(name string, group *Group, force bool) (*DeploymentID, error) {
deploymentID := new(DeploymentID)
uri := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
if force {
uri = uri + "?force=true"
path += "?force=true"
}
if err := r.apiPut(uri, group, deploymentID); err != nil {
if err := r.apiPut(path, group, deploymentID); err != nil {
return nil, err
}

View File

@@ -27,7 +27,7 @@ type HealthCheck struct {
GracePeriodSeconds int `json:"gracePeriodSeconds,omitempty"`
IntervalSeconds int `json:"intervalSeconds,omitempty"`
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
IgnoreHTTP1xx *bool `json:"ignoreHttp1xx,ommitempty"`
IgnoreHTTP1xx *bool `json:"ignoreHttp1xx,omitempty"`
}
// SetCommand sets the given command on the health check.

View File

@@ -51,8 +51,8 @@ func (r *marathonClient) Queue() (*Queue, error) {
// DeleteQueueDelay resets task launch delay of the specific application
// appID: the ID of the application
func (r *marathonClient) DeleteQueueDelay(appID string) error {
uri := fmt.Sprintf("%s/%s/delay", marathonAPIQueue, trimRootPath(appID))
err := r.apiDelete(uri, nil, nil)
path := fmt.Sprintf("%s/%s/delay", marathonAPIQueue, trimRootPath(appID))
err := r.apiDelete(path, nil, nil)
if err != nil {
return err
}

View File

@@ -103,7 +103,8 @@ func (r *marathonClient) registerSubscription() error {
case EventsTransportCallback:
return r.registerCallbackSubscription()
case EventsTransportSSE:
return r.registerSSESubscription()
r.registerSSESubscription()
return nil
default:
return fmt.Errorf("the events transport: %d is not supported", r.config.EventsTransport)
}
@@ -162,47 +163,88 @@ func (r *marathonClient) registerCallbackSubscription() error {
return nil
}
func (r *marathonClient) registerSSESubscription() error {
// Prevent multiple SSE subscriptions
// registerSSESubscription starts a go routine that continously tries to
// connect to the SSE stream and to process the received events. To establish
// the connection it tries the active cluster members until no more member is
// active. When this happens it will retry to get a connection every 5 seconds.
func (r *marathonClient) registerSSESubscription() {
if r.subscribedToSSE {
return nil
}
request, _, err := r.buildAPIRequest("GET", marathonAPIEventStream, nil)
if err != nil {
return err
}
// Try to connect to stream, reusing the http client settings
stream, err := eventsource.SubscribeWith("", r.config.HTTPClient, request)
if err != nil {
return err
return
}
go func() {
for {
select {
case ev := <-stream.Events:
if err := r.handleEvent(ev.Data()); err != nil {
// TODO let the user handle this error instead of logging it here
r.debugLog.Printf("registerSSESubscription(): failed to handle event: %v\n", err)
}
case err := <-stream.Errors:
// TODO let the user handle this error instead of logging it here
r.debugLog.Printf("registerSSESubscription(): failed to receive event: %v\n", err)
stream, err := r.connectToSSE()
if err != nil {
r.debugLog.Printf("Error connecting SSE subscription: %s", err)
<-time.After(5 * time.Second)
continue
}
err = r.listenToSSE(stream)
stream.Close()
r.debugLog.Printf("Error on SSE subscription: %s", err)
}
}()
r.subscribedToSSE = true
return nil
}
// connectToSSE tries to establish an *eventsource.Stream to any of the Marathon cluster members, marking the
// member as down on connection failure, until there is no more active member in the cluster.
// Given the http request can not be built, it will panic as this case should never happen.
func (r *marathonClient) connectToSSE() (*eventsource.Stream, error) {
for {
request, member, err := r.buildAPIRequest("GET", marathonAPIEventStream, nil)
if err != nil {
switch err.(type) {
case newRequestError:
panic(fmt.Sprintf("Requests for SSE subscriptions should never fail to be created: %s", err.Error()))
default:
return nil, err
}
}
// The event source library manipulates the HTTPClient. So we create a new one and copy
// its underlying fields for performance reasons. See note that at least the Transport
// should be reused here: https://golang.org/pkg/net/http/#Client
httpClient := &http.Client{
Transport: r.config.HTTPClient.Transport,
CheckRedirect: r.config.HTTPClient.CheckRedirect,
Jar: r.config.HTTPClient.Jar,
Timeout: r.config.HTTPClient.Timeout,
}
stream, err := eventsource.SubscribeWith("", httpClient, request)
if err != nil {
r.debugLog.Printf("Error subscribing to Marathon event stream: %s", err)
r.hosts.markDown(member)
continue
}
return stream, nil
}
}
func (r *marathonClient) listenToSSE(stream *eventsource.Stream) error {
for {
select {
case ev := <-stream.Events:
if err := r.handleEvent(ev.Data()); err != nil {
r.debugLog.Printf("listenToSSE(): failed to handle event: %v", err)
}
case err := <-stream.Errors:
return err
}
}
}
// Subscribe adds a URL to Marathon's callback facility
// callback : the URL you wish to subscribe
func (r *marathonClient) Subscribe(callback string) error {
uri := fmt.Sprintf("%s?callbackUrl=%s", marathonAPISubscription, callback)
return r.apiPost(uri, "", nil)
path := fmt.Sprintf("%s?callbackUrl=%s", marathonAPISubscription, callback)
return r.apiPost(path, "", nil)
}

View File

@@ -79,13 +79,13 @@ func (r *Task) HasHealthCheckResults() bool {
// AllTasks lists tasks of all applications.
// opts: AllTasksOpts request payload
func (r *marathonClient) AllTasks(opts *AllTasksOpts) (*Tasks, error) {
u, err := addOptions(marathonAPITasks, opts)
path, err := addOptions(marathonAPITasks, opts)
if err != nil {
return nil, err
}
tasks := new(Tasks)
if err := r.apiGet(u, nil, tasks); err != nil {
if err := r.apiGet(path, nil, tasks); err != nil {
return nil, err
}
@@ -107,14 +107,14 @@ func (r *marathonClient) Tasks(id string) (*Tasks, error) {
// id: the id of the application
// opts: KillApplicationTasksOpts request payload
func (r *marathonClient) KillApplicationTasks(id string, opts *KillApplicationTasksOpts) (*Tasks, error) {
u := fmt.Sprintf("%s/%s/tasks", marathonAPIApps, trimRootPath(id))
u, err := addOptions(u, opts)
path := fmt.Sprintf("%s/%s/tasks", marathonAPIApps, trimRootPath(id))
path, err := addOptions(path, opts)
if err != nil {
return nil, err
}
tasks := new(Tasks)
if err := r.apiDelete(u, nil, tasks); err != nil {
if err := r.apiDelete(path, nil, tasks); err != nil {
return nil, err
}
@@ -129,8 +129,8 @@ func (r *marathonClient) KillTask(taskID string, opts *KillTaskOpts) (*Task, err
appName = strings.Replace(appName, "_", "/", -1)
taskID = strings.Replace(taskID, "/", "_", -1)
u := fmt.Sprintf("%s/%s/tasks/%s", marathonAPIApps, appName, taskID)
u, err := addOptions(u, opts)
path := fmt.Sprintf("%s/%s/tasks/%s", marathonAPIApps, appName, taskID)
path, err := addOptions(path, opts)
if err != nil {
return nil, err
}
@@ -139,7 +139,7 @@ func (r *marathonClient) KillTask(taskID string, opts *KillTaskOpts) (*Task, err
Task Task `json:"task"`
})
if err := r.apiDelete(u, nil, wrappedTask); err != nil {
if err := r.apiDelete(path, nil, wrappedTask); err != nil {
return nil, err
}
@@ -150,8 +150,8 @@ func (r *marathonClient) KillTask(taskID string, opts *KillTaskOpts) (*Task, err
// tasks: the array of task ids
// opts: KillTaskOpts request payload
func (r *marathonClient) KillTasks(tasks []string, opts *KillTaskOpts) error {
u := fmt.Sprintf("%s/delete", marathonAPITasks)
u, err := addOptions(u, opts)
path := fmt.Sprintf("%s/delete", marathonAPITasks)
path, err := addOptions(path, opts)
if err != nil {
return nil
}
@@ -161,7 +161,7 @@ func (r *marathonClient) KillTasks(tasks []string, opts *KillTaskOpts) error {
}
post.IDs = tasks
return r.apiPost(u, &post, nil)
return r.apiPost(path, &post, nil)
}
// TaskEndpoints gets the endpoints i.e. HOST_IP:DYNAMIC_PORT for a specific application service

View File

@@ -0,0 +1,77 @@
/*
Copyright 2017 Rohith All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package marathon
import (
"encoding/json"
"fmt"
)
const UnreachableStrategyAbsenceReasonDisabled = "disabled"
// UnreachableStrategy is the unreachable strategy applied to an application.
type UnreachableStrategy struct {
EnabledUnreachableStrategy
AbsenceReason string
}
// EnabledUnreachableStrategy covers parameters pertaining to present unreachable strategies.
type EnabledUnreachableStrategy struct {
InactiveAfterSeconds *float64 `json:"inactiveAfterSeconds,omitempty"`
ExpungeAfterSeconds *float64 `json:"expungeAfterSeconds,omitempty"`
}
type unreachableStrategy UnreachableStrategy
// UnmarshalJSON unmarshals the given JSON into an UnreachableStrategy. It
// populates parameters for present strategies, and otherwise only sets the
// absence reason.
func (us *UnreachableStrategy) UnmarshalJSON(b []byte) error {
var u unreachableStrategy
var errEnabledUS, errNonEnabledUS error
if errEnabledUS = json.Unmarshal(b, &u); errEnabledUS == nil {
*us = UnreachableStrategy(u)
return nil
}
if errNonEnabledUS = json.Unmarshal(b, &us.AbsenceReason); errNonEnabledUS == nil {
return nil
}
return fmt.Errorf("failed to unmarshal unreachable strategy: unmarshaling into enabled returned error '%s'; unmarshaling into non-enabled returned error '%s'", errEnabledUS, errNonEnabledUS)
}
// MarshalJSON marshals the unreachable strategy.
func (us *UnreachableStrategy) MarshalJSON() ([]byte, error) {
if us.AbsenceReason == "" {
return json.Marshal(us.EnabledUnreachableStrategy)
}
return json.Marshal(us.AbsenceReason)
}
// SetInactiveAfterSeconds sets the period after which instance will be marked as inactive.
func (us UnreachableStrategy) SetInactiveAfterSeconds(cap float64) UnreachableStrategy {
us.InactiveAfterSeconds = &cap
return us
}
// SetExpungeAfterSeconds sets the period after which instance will be expunged.
func (us UnreachableStrategy) SetExpungeAfterSeconds(cap float64) UnreachableStrategy {
us.ExpungeAfterSeconds = &cap
return us
}

View File

@@ -6,9 +6,8 @@
//
// Overview
//
// The Conn type represents a WebSocket connection. A server application uses
// the Upgrade function from an Upgrader object with a HTTP request handler
// to get a pointer to a Conn:
// The Conn type represents a WebSocket connection. A server application calls
// the Upgrader.Upgrade method from an HTTP request handler to get a *Conn:
//
// var upgrader = websocket.Upgrader{
// ReadBufferSize: 1024,
@@ -33,7 +32,7 @@
// if err != nil {
// return
// }
// if err = conn.WriteMessage(messageType, p); err != nil {
// if err := conn.WriteMessage(messageType, p); err != nil {
// return err
// }
// }
@@ -147,9 +146,9 @@
// CheckOrigin: func(r *http.Request) bool { return true },
// }
//
// The deprecated Upgrade function does not enforce an origin policy. It's the
// application's responsibility to check the Origin header before calling
// Upgrade.
// The deprecated package-level Upgrade function does not perform origin
// checking. The application is responsible for checking the Origin header
// before calling the Upgrade function.
//
// Compression EXPERIMENTAL
//

View File

@@ -9,12 +9,14 @@ import (
"io"
)
// WriteJSON is deprecated, use c.WriteJSON instead.
// WriteJSON writes the JSON encoding of v as a message.
//
// Deprecated: Use c.WriteJSON instead.
func WriteJSON(c *Conn, v interface{}) error {
return c.WriteJSON(v)
}
// WriteJSON writes the JSON encoding of v to the connection.
// WriteJSON writes the JSON encoding of v as a message.
//
// See the documentation for encoding/json Marshal for details about the
// conversion of Go values to JSON.
@@ -31,7 +33,10 @@ func (c *Conn) WriteJSON(v interface{}) error {
return err2
}
// ReadJSON is deprecated, use c.ReadJSON instead.
// ReadJSON reads the next JSON-encoded message from the connection and stores
// it in the value pointed to by v.
//
// Deprecated: Use c.ReadJSON instead.
func ReadJSON(c *Conn, v interface{}) error {
return c.ReadJSON(v)
}

View File

@@ -230,10 +230,11 @@ func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeade
// Upgrade upgrades the HTTP server connection to the WebSocket protocol.
//
// This function is deprecated, use websocket.Upgrader instead.
// Deprecated: Use websocket.Upgrader instead.
//
// The application is responsible for checking the request origin before
// calling Upgrade. An example implementation of the same origin policy is:
// Upgrade does not perform origin checking. The application is responsible for
// checking the Origin header before calling Upgrade. An example implementation
// of the same origin policy check is:
//
// if req.Header.Get("Origin") != "http://"+req.Host {
// http.Error(w, "Origin not allowed", 403)

View File

@@ -111,14 +111,14 @@ func nextTokenOrQuoted(s string) (value string, rest string) {
case escape:
escape = false
p[j] = b
j += 1
j++
case b == '\\':
escape = true
case b == '"':
return string(p[:j]), s[i+1:]
default:
p[j] = b
j += 1
j++
}
}
return "", ""

28
vendor/github.com/stretchr/testify/require/doc.go generated vendored Normal file
View File

@@ -0,0 +1,28 @@
// Package require implements the same assertions as the `assert` package but
// stops test execution when a test fails.
//
// Example Usage
//
// The following is a complete example using require in a standard test function:
// import (
// "testing"
// "github.com/stretchr/testify/require"
// )
//
// func TestSomething(t *testing.T) {
//
// var a string = "Hello"
// var b string = "Hello"
//
// require.Equal(t, a, b, "The two words should be the same.")
//
// }
//
// Assertions
//
// The `require` package have same global functions as in the `assert` package,
// but instead of returning a boolean result they call `t.FailNow()`.
//
// Every assertion function also takes an optional string message as the final argument,
// allowing custom error messages to be appended to the message the assertion method outputs.
package require

View File

@@ -0,0 +1,16 @@
package require
// Assertions provides assertion methods around the
// TestingT interface.
type Assertions struct {
t TestingT
}
// New makes a new Assertions object for the specified TestingT.
func New(t TestingT) *Assertions {
return &Assertions{
t: t,
}
}
//go:generate go run ../_codegen/main.go -output-package=require -template=require_forward.go.tmpl

429
vendor/github.com/stretchr/testify/require/require.go generated vendored Normal file
View File

@@ -0,0 +1,429 @@
/*
* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen
* THIS FILE MUST NOT BE EDITED BY HAND
*/
package require
import (
assert "github.com/stretchr/testify/assert"
http "net/http"
url "net/url"
time "time"
)
// Condition uses a Comparison to assert a complex condition.
func Condition(t TestingT, comp assert.Comparison, msgAndArgs ...interface{}) {
if !assert.Condition(t, comp, msgAndArgs...) {
t.FailNow()
}
}
// Contains asserts that the specified string, list(array, slice...) or map contains the
// specified substring or element.
//
// assert.Contains(t, "Hello World", "World", "But 'Hello World' does contain 'World'")
// assert.Contains(t, ["Hello", "World"], "World", "But ["Hello", "World"] does contain 'World'")
// assert.Contains(t, {"Hello": "World"}, "Hello", "But {'Hello': 'World'} does contain 'Hello'")
//
// Returns whether the assertion was successful (true) or not (false).
func Contains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) {
if !assert.Contains(t, s, contains, msgAndArgs...) {
t.FailNow()
}
}
// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either
// a slice or a channel with len == 0.
//
// assert.Empty(t, obj)
//
// Returns whether the assertion was successful (true) or not (false).
func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) {
if !assert.Empty(t, object, msgAndArgs...) {
t.FailNow()
}
}
// Equal asserts that two objects are equal.
//
// assert.Equal(t, 123, 123, "123 and 123 should be equal")
//
// Returns whether the assertion was successful (true) or not (false).
//
// Pointer variable equality is determined based on the equality of the
// referenced values (as opposed to the memory addresses).
func Equal(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
if !assert.Equal(t, expected, actual, msgAndArgs...) {
t.FailNow()
}
}
// EqualError asserts that a function returned an error (i.e. not `nil`)
// and that it is equal to the provided error.
//
// actualObj, err := SomeFunction()
// assert.EqualError(t, err, expectedErrorString, "An error was expected")
//
// Returns whether the assertion was successful (true) or not (false).
func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) {
if !assert.EqualError(t, theError, errString, msgAndArgs...) {
t.FailNow()
}
}
// EqualValues asserts that two objects are equal or convertable to the same types
// and equal.
//
// assert.EqualValues(t, uint32(123), int32(123), "123 and 123 should be equal")
//
// Returns whether the assertion was successful (true) or not (false).
func EqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
if !assert.EqualValues(t, expected, actual, msgAndArgs...) {
t.FailNow()
}
}
// Error asserts that a function returned an error (i.e. not `nil`).
//
// actualObj, err := SomeFunction()
// if assert.Error(t, err, "An error was expected") {
// assert.Equal(t, err, expectedError)
// }
//
// Returns whether the assertion was successful (true) or not (false).
func Error(t TestingT, err error, msgAndArgs ...interface{}) {
if !assert.Error(t, err, msgAndArgs...) {
t.FailNow()
}
}
// Exactly asserts that two objects are equal is value and type.
//
// assert.Exactly(t, int32(123), int64(123), "123 and 123 should NOT be equal")
//
// Returns whether the assertion was successful (true) or not (false).
func Exactly(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
if !assert.Exactly(t, expected, actual, msgAndArgs...) {
t.FailNow()
}
}
// Fail reports a failure through
func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) {
if !assert.Fail(t, failureMessage, msgAndArgs...) {
t.FailNow()
}
}
// FailNow fails test
func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) {
if !assert.FailNow(t, failureMessage, msgAndArgs...) {
t.FailNow()
}
}
// False asserts that the specified value is false.
//
// assert.False(t, myBool, "myBool should be false")
//
// Returns whether the assertion was successful (true) or not (false).
func False(t TestingT, value bool, msgAndArgs ...interface{}) {
if !assert.False(t, value, msgAndArgs...) {
t.FailNow()
}
}
// HTTPBodyContains asserts that a specified handler returns a
// body that contains a string.
//
// assert.HTTPBodyContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky")
//
// Returns whether the assertion was successful (true) or not (false).
func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}) {
if !assert.HTTPBodyContains(t, handler, method, url, values, str) {
t.FailNow()
}
}
// HTTPBodyNotContains asserts that a specified handler returns a
// body that does not contain a string.
//
// assert.HTTPBodyNotContains(t, myHandler, "www.google.com", nil, "I'm Feeling Lucky")
//
// Returns whether the assertion was successful (true) or not (false).
func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}) {
if !assert.HTTPBodyNotContains(t, handler, method, url, values, str) {
t.FailNow()
}
}
// HTTPError asserts that a specified handler returns an error status code.
//
// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
//
// Returns whether the assertion was successful (true) or not (false).
func HTTPError(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values) {
if !assert.HTTPError(t, handler, method, url, values) {
t.FailNow()
}
}
// HTTPRedirect asserts that a specified handler returns a redirect status code.
//
// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
//
// Returns whether the assertion was successful (true) or not (false).
func HTTPRedirect(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values) {
if !assert.HTTPRedirect(t, handler, method, url, values) {
t.FailNow()
}
}
// HTTPSuccess asserts that a specified handler returns a success status code.
//
// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil)
//
// Returns whether the assertion was successful (true) or not (false).
func HTTPSuccess(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values) {
if !assert.HTTPSuccess(t, handler, method, url, values) {
t.FailNow()
}
}
// Implements asserts that an object is implemented by the specified interface.
//
// assert.Implements(t, (*MyInterface)(nil), new(MyObject), "MyObject")
func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) {
if !assert.Implements(t, interfaceObject, object, msgAndArgs...) {
t.FailNow()
}
}
// InDelta asserts that the two numerals are within delta of each other.
//
// assert.InDelta(t, math.Pi, (22 / 7.0), 0.01)
//
// Returns whether the assertion was successful (true) or not (false).
func InDelta(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) {
if !assert.InDelta(t, expected, actual, delta, msgAndArgs...) {
t.FailNow()
}
}
// InDeltaSlice is the same as InDelta, except it compares two slices.
func InDeltaSlice(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) {
if !assert.InDeltaSlice(t, expected, actual, delta, msgAndArgs...) {
t.FailNow()
}
}
// InEpsilon asserts that expected and actual have a relative error less than epsilon
//
// Returns whether the assertion was successful (true) or not (false).
func InEpsilon(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) {
if !assert.InEpsilon(t, expected, actual, epsilon, msgAndArgs...) {
t.FailNow()
}
}
// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices.
func InEpsilonSlice(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) {
if !assert.InEpsilonSlice(t, expected, actual, epsilon, msgAndArgs...) {
t.FailNow()
}
}
// IsType asserts that the specified objects are of the same type.
func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) {
if !assert.IsType(t, expectedType, object, msgAndArgs...) {
t.FailNow()
}
}
// JSONEq asserts that two JSON strings are equivalent.
//
// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)
//
// Returns whether the assertion was successful (true) or not (false).
func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) {
if !assert.JSONEq(t, expected, actual, msgAndArgs...) {
t.FailNow()
}
}
// Len asserts that the specified object has specific length.
// Len also fails if the object has a type that len() not accept.
//
// assert.Len(t, mySlice, 3, "The size of slice is not 3")
//
// Returns whether the assertion was successful (true) or not (false).
func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) {
if !assert.Len(t, object, length, msgAndArgs...) {
t.FailNow()
}
}
// Nil asserts that the specified object is nil.
//
// assert.Nil(t, err, "err should be nothing")
//
// Returns whether the assertion was successful (true) or not (false).
func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) {
if !assert.Nil(t, object, msgAndArgs...) {
t.FailNow()
}
}
// NoError asserts that a function returned no error (i.e. `nil`).
//
// actualObj, err := SomeFunction()
// if assert.NoError(t, err) {
// assert.Equal(t, actualObj, expectedObj)
// }
//
// Returns whether the assertion was successful (true) or not (false).
func NoError(t TestingT, err error, msgAndArgs ...interface{}) {
if !assert.NoError(t, err, msgAndArgs...) {
t.FailNow()
}
}
// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the
// specified substring or element.
//
// assert.NotContains(t, "Hello World", "Earth", "But 'Hello World' does NOT contain 'Earth'")
// assert.NotContains(t, ["Hello", "World"], "Earth", "But ['Hello', 'World'] does NOT contain 'Earth'")
// assert.NotContains(t, {"Hello": "World"}, "Earth", "But {'Hello': 'World'} does NOT contain 'Earth'")
//
// Returns whether the assertion was successful (true) or not (false).
func NotContains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) {
if !assert.NotContains(t, s, contains, msgAndArgs...) {
t.FailNow()
}
}
// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either
// a slice or a channel with len == 0.
//
// if assert.NotEmpty(t, obj) {
// assert.Equal(t, "two", obj[1])
// }
//
// Returns whether the assertion was successful (true) or not (false).
func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) {
if !assert.NotEmpty(t, object, msgAndArgs...) {
t.FailNow()
}
}
// NotEqual asserts that the specified values are NOT equal.
//
// assert.NotEqual(t, obj1, obj2, "two objects shouldn't be equal")
//
// Returns whether the assertion was successful (true) or not (false).
//
// Pointer variable equality is determined based on the equality of the
// referenced values (as opposed to the memory addresses).
func NotEqual(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
if !assert.NotEqual(t, expected, actual, msgAndArgs...) {
t.FailNow()
}
}
// NotNil asserts that the specified object is not nil.
//
// assert.NotNil(t, err, "err should be something")
//
// Returns whether the assertion was successful (true) or not (false).
func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) {
if !assert.NotNil(t, object, msgAndArgs...) {
t.FailNow()
}
}
// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic.
//
// assert.NotPanics(t, func(){
// RemainCalm()
// }, "Calling RemainCalm() should NOT panic")
//
// Returns whether the assertion was successful (true) or not (false).
func NotPanics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) {
if !assert.NotPanics(t, f, msgAndArgs...) {
t.FailNow()
}
}
// NotRegexp asserts that a specified regexp does not match a string.
//
// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting")
// assert.NotRegexp(t, "^start", "it's not starting")
//
// Returns whether the assertion was successful (true) or not (false).
func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) {
if !assert.NotRegexp(t, rx, str, msgAndArgs...) {
t.FailNow()
}
}
// NotZero asserts that i is not the zero value for its type and returns the truth.
func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) {
if !assert.NotZero(t, i, msgAndArgs...) {
t.FailNow()
}
}
// Panics asserts that the code inside the specified PanicTestFunc panics.
//
// assert.Panics(t, func(){
// GoCrazy()
// }, "Calling GoCrazy() should panic")
//
// Returns whether the assertion was successful (true) or not (false).
func Panics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) {
if !assert.Panics(t, f, msgAndArgs...) {
t.FailNow()
}
}
// Regexp asserts that a specified regexp matches a string.
//
// assert.Regexp(t, regexp.MustCompile("start"), "it's starting")
// assert.Regexp(t, "start...$", "it's not starting")
//
// Returns whether the assertion was successful (true) or not (false).
func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) {
if !assert.Regexp(t, rx, str, msgAndArgs...) {
t.FailNow()
}
}
// True asserts that the specified value is true.
//
// assert.True(t, myBool, "myBool should be true")
//
// Returns whether the assertion was successful (true) or not (false).
func True(t TestingT, value bool, msgAndArgs ...interface{}) {
if !assert.True(t, value, msgAndArgs...) {
t.FailNow()
}
}
// WithinDuration asserts that the two times are within duration delta of each other.
//
// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second, "The difference should not be more than 10s")
//
// Returns whether the assertion was successful (true) or not (false).
func WithinDuration(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) {
if !assert.WithinDuration(t, expected, actual, delta, msgAndArgs...) {
t.FailNow()
}
}
// Zero asserts that i is the zero value for its type and returns the truth.
func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) {
if !assert.Zero(t, i, msgAndArgs...) {
t.FailNow()
}
}

View File

@@ -0,0 +1,353 @@
/*
* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen
* THIS FILE MUST NOT BE EDITED BY HAND
*/
package require
import (
assert "github.com/stretchr/testify/assert"
http "net/http"
url "net/url"
time "time"
)
// Condition uses a Comparison to assert a complex condition.
func (a *Assertions) Condition(comp assert.Comparison, msgAndArgs ...interface{}) {
Condition(a.t, comp, msgAndArgs...)
}
// Contains asserts that the specified string, list(array, slice...) or map contains the
// specified substring or element.
//
// a.Contains("Hello World", "World", "But 'Hello World' does contain 'World'")
// a.Contains(["Hello", "World"], "World", "But ["Hello", "World"] does contain 'World'")
// a.Contains({"Hello": "World"}, "Hello", "But {'Hello': 'World'} does contain 'Hello'")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) {
Contains(a.t, s, contains, msgAndArgs...)
}
// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either
// a slice or a channel with len == 0.
//
// a.Empty(obj)
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) {
Empty(a.t, object, msgAndArgs...)
}
// Equal asserts that two objects are equal.
//
// a.Equal(123, 123, "123 and 123 should be equal")
//
// Returns whether the assertion was successful (true) or not (false).
//
// Pointer variable equality is determined based on the equality of the
// referenced values (as opposed to the memory addresses).
func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
Equal(a.t, expected, actual, msgAndArgs...)
}
// EqualError asserts that a function returned an error (i.e. not `nil`)
// and that it is equal to the provided error.
//
// actualObj, err := SomeFunction()
// a.EqualError(err, expectedErrorString, "An error was expected")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) {
EqualError(a.t, theError, errString, msgAndArgs...)
}
// EqualValues asserts that two objects are equal or convertable to the same types
// and equal.
//
// a.EqualValues(uint32(123), int32(123), "123 and 123 should be equal")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
EqualValues(a.t, expected, actual, msgAndArgs...)
}
// Error asserts that a function returned an error (i.e. not `nil`).
//
// actualObj, err := SomeFunction()
// if a.Error(err, "An error was expected") {
// assert.Equal(t, err, expectedError)
// }
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) Error(err error, msgAndArgs ...interface{}) {
Error(a.t, err, msgAndArgs...)
}
// Exactly asserts that two objects are equal is value and type.
//
// a.Exactly(int32(123), int64(123), "123 and 123 should NOT be equal")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
Exactly(a.t, expected, actual, msgAndArgs...)
}
// Fail reports a failure through
func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) {
Fail(a.t, failureMessage, msgAndArgs...)
}
// FailNow fails test
func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) {
FailNow(a.t, failureMessage, msgAndArgs...)
}
// False asserts that the specified value is false.
//
// a.False(myBool, "myBool should be false")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) False(value bool, msgAndArgs ...interface{}) {
False(a.t, value, msgAndArgs...)
}
// HTTPBodyContains asserts that a specified handler returns a
// body that contains a string.
//
// a.HTTPBodyContains(myHandler, "www.google.com", nil, "I'm Feeling Lucky")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}) {
HTTPBodyContains(a.t, handler, method, url, values, str)
}
// HTTPBodyNotContains asserts that a specified handler returns a
// body that does not contain a string.
//
// a.HTTPBodyNotContains(myHandler, "www.google.com", nil, "I'm Feeling Lucky")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}) {
HTTPBodyNotContains(a.t, handler, method, url, values, str)
}
// HTTPError asserts that a specified handler returns an error status code.
//
// a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values) {
HTTPError(a.t, handler, method, url, values)
}
// HTTPRedirect asserts that a specified handler returns a redirect status code.
//
// a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values) {
HTTPRedirect(a.t, handler, method, url, values)
}
// HTTPSuccess asserts that a specified handler returns a success status code.
//
// a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil)
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values) {
HTTPSuccess(a.t, handler, method, url, values)
}
// Implements asserts that an object is implemented by the specified interface.
//
// a.Implements((*MyInterface)(nil), new(MyObject), "MyObject")
func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) {
Implements(a.t, interfaceObject, object, msgAndArgs...)
}
// InDelta asserts that the two numerals are within delta of each other.
//
// a.InDelta(math.Pi, (22 / 7.0), 0.01)
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) {
InDelta(a.t, expected, actual, delta, msgAndArgs...)
}
// InDeltaSlice is the same as InDelta, except it compares two slices.
func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) {
InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...)
}
// InEpsilon asserts that expected and actual have a relative error less than epsilon
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) {
InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...)
}
// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices.
func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) {
InEpsilonSlice(a.t, expected, actual, epsilon, msgAndArgs...)
}
// IsType asserts that the specified objects are of the same type.
func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) {
IsType(a.t, expectedType, object, msgAndArgs...)
}
// JSONEq asserts that two JSON strings are equivalent.
//
// a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) {
JSONEq(a.t, expected, actual, msgAndArgs...)
}
// Len asserts that the specified object has specific length.
// Len also fails if the object has a type that len() not accept.
//
// a.Len(mySlice, 3, "The size of slice is not 3")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) {
Len(a.t, object, length, msgAndArgs...)
}
// Nil asserts that the specified object is nil.
//
// a.Nil(err, "err should be nothing")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) {
Nil(a.t, object, msgAndArgs...)
}
// NoError asserts that a function returned no error (i.e. `nil`).
//
// actualObj, err := SomeFunction()
// if a.NoError(err) {
// assert.Equal(t, actualObj, expectedObj)
// }
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) {
NoError(a.t, err, msgAndArgs...)
}
// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the
// specified substring or element.
//
// a.NotContains("Hello World", "Earth", "But 'Hello World' does NOT contain 'Earth'")
// a.NotContains(["Hello", "World"], "Earth", "But ['Hello', 'World'] does NOT contain 'Earth'")
// a.NotContains({"Hello": "World"}, "Earth", "But {'Hello': 'World'} does NOT contain 'Earth'")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) {
NotContains(a.t, s, contains, msgAndArgs...)
}
// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either
// a slice or a channel with len == 0.
//
// if a.NotEmpty(obj) {
// assert.Equal(t, "two", obj[1])
// }
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) {
NotEmpty(a.t, object, msgAndArgs...)
}
// NotEqual asserts that the specified values are NOT equal.
//
// a.NotEqual(obj1, obj2, "two objects shouldn't be equal")
//
// Returns whether the assertion was successful (true) or not (false).
//
// Pointer variable equality is determined based on the equality of the
// referenced values (as opposed to the memory addresses).
func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) {
NotEqual(a.t, expected, actual, msgAndArgs...)
}
// NotNil asserts that the specified object is not nil.
//
// a.NotNil(err, "err should be something")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) {
NotNil(a.t, object, msgAndArgs...)
}
// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic.
//
// a.NotPanics(func(){
// RemainCalm()
// }, "Calling RemainCalm() should NOT panic")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) NotPanics(f assert.PanicTestFunc, msgAndArgs ...interface{}) {
NotPanics(a.t, f, msgAndArgs...)
}
// NotRegexp asserts that a specified regexp does not match a string.
//
// a.NotRegexp(regexp.MustCompile("starts"), "it's starting")
// a.NotRegexp("^start", "it's not starting")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) {
NotRegexp(a.t, rx, str, msgAndArgs...)
}
// NotZero asserts that i is not the zero value for its type and returns the truth.
func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) {
NotZero(a.t, i, msgAndArgs...)
}
// Panics asserts that the code inside the specified PanicTestFunc panics.
//
// a.Panics(func(){
// GoCrazy()
// }, "Calling GoCrazy() should panic")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) Panics(f assert.PanicTestFunc, msgAndArgs ...interface{}) {
Panics(a.t, f, msgAndArgs...)
}
// Regexp asserts that a specified regexp matches a string.
//
// a.Regexp(regexp.MustCompile("start"), "it's starting")
// a.Regexp("start...$", "it's not starting")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) {
Regexp(a.t, rx, str, msgAndArgs...)
}
// True asserts that the specified value is true.
//
// a.True(myBool, "myBool should be true")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) True(value bool, msgAndArgs ...interface{}) {
True(a.t, value, msgAndArgs...)
}
// WithinDuration asserts that the two times are within duration delta of each other.
//
// a.WithinDuration(time.Now(), time.Now(), 10*time.Second, "The difference should not be more than 10s")
//
// Returns whether the assertion was successful (true) or not (false).
func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) {
WithinDuration(a.t, expected, actual, delta, msgAndArgs...)
}
// Zero asserts that i is the zero value for its type and returns the truth.
func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) {
Zero(a.t, i, msgAndArgs...)
}

View File

@@ -0,0 +1,9 @@
package require
// TestingT is an interface wrapper around *testing.T
type TestingT interface {
Errorf(format string, args ...interface{})
FailNow()
}
//go:generate go run ../_codegen/main.go -output-package=require -template=require.go.tmpl

View File

@@ -6,15 +6,14 @@ package forward
import (
"crypto/tls"
"io"
"net"
"net/http"
"net/url"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/vulcand/oxy/utils"
)
@@ -158,7 +157,9 @@ func (f *Forwarder) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// serveHTTP forwards HTTP traffic using the configured transport
func (f *httpForwarder) serveHTTP(w http.ResponseWriter, req *http.Request, ctx *handlerContext) {
start := time.Now().UTC()
response, err := f.roundTripper.RoundTrip(f.copyRequest(req, req.URL))
if err != nil {
ctx.log.Errorf("Error forwarding to %v, err: %v", req.URL, err)
ctx.errHandler.ServeHTTP(w, req, err)
@@ -168,6 +169,16 @@ func (f *httpForwarder) serveHTTP(w http.ResponseWriter, req *http.Request, ctx
utils.CopyHeaders(w.Header(), response.Header)
// Remove hop-by-hop headers.
utils.RemoveHeaders(w.Header(), HopHeaders...)
announcedTrailerKeyCount := len(response.Trailer)
if announcedTrailerKeyCount > 0 {
trailerKeys := make([]string, 0, announcedTrailerKeyCount)
for k := range response.Trailer {
trailerKeys = append(trailerKeys, k)
}
w.Header().Add("Trailer", strings.Join(trailerKeys, ", "))
}
w.WriteHeader(response.StatusCode)
stream := f.streamResponse
@@ -178,6 +189,20 @@ func (f *httpForwarder) serveHTTP(w http.ResponseWriter, req *http.Request, ctx
}
}
written, err := io.Copy(newResponseFlusher(w, stream), response.Body)
if err != nil {
ctx.log.Errorf("Error copying upstream response body: %v", err)
ctx.errHandler.ServeHTTP(w, req, err)
return
}
defer response.Body.Close()
forceSetTrailers := len(response.Trailer) != announcedTrailerKeyCount
shallowCopyTrailers(w.Header(), response.Trailer, forceSetTrailers)
if written != 0 {
w.Header().Set(ContentLength, strconv.FormatInt(written, 10))
}
if req.TLS != nil {
ctx.log.Infof("Round trip: %v, code: %v, duration: %v tls:version: %x, tls:resume:%t, tls:csuite:%x, tls:server:%v",
@@ -191,17 +216,6 @@ func (f *httpForwarder) serveHTTP(w http.ResponseWriter, req *http.Request, ctx
req.URL, response.StatusCode, time.Now().UTC().Sub(start))
}
defer response.Body.Close()
if err != nil {
ctx.log.Errorf("Error copying upstream response Body: %v", err)
ctx.errHandler.ServeHTTP(w, req, err)
return
}
if written != 0 {
w.Header().Set(ContentLength, strconv.FormatInt(written, 10))
}
}
// copyRequest makes a copy of the specified request to be sent using the configured
@@ -239,65 +253,51 @@ func (f *httpForwarder) copyRequest(req *http.Request, u *url.URL) *http.Request
// serveHTTP forwards websocket traffic
func (f *websocketForwarder) serveHTTP(w http.ResponseWriter, req *http.Request, ctx *handlerContext) {
outReq := f.copyRequest(req, req.URL)
host := outReq.URL.Host
dial := net.Dial
// if host does not specify a port, use the default http port
if !strings.Contains(host, ":") {
if outReq.URL.Scheme == "wss" {
host = host + ":443"
} else {
host = host + ":80"
}
dialer := websocket.DefaultDialer
if outReq.URL.Scheme == "wss" && f.TLSClientConfig != nil {
dialer.TLSClientConfig = f.TLSClientConfig
}
if outReq.URL.Scheme == "wss" {
if f.TLSClientConfig == nil {
f.TLSClientConfig = http.DefaultTransport.(*http.Transport).TLSClientConfig
}
dial = func(network, address string) (net.Conn, error) {
return tls.Dial("tcp", host, f.TLSClientConfig)
}
}
targetConn, err := dial("tcp", host)
targetConn, resp, err := dialer.Dial(outReq.URL.String(), outReq.Header)
if err != nil {
ctx.log.Errorf("Error dialing `%v`: %v", host, err)
ctx.log.Errorf("Error dialing `%v`: %v", outReq.Host, err)
ctx.errHandler.ServeHTTP(w, req, err)
return
}
hijacker, ok := w.(http.Hijacker)
if !ok {
ctx.log.Errorf("Unable to hijack the connection: %v", reflect.TypeOf(w))
ctx.errHandler.ServeHTTP(w, req, nil)
return
}
underlyingConn, _, err := hijacker.Hijack()
//Only the targetConn choose to CheckOrigin or not
upgrader := websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
return true
}}
utils.RemoveHeaders(resp.Header, WebsocketUpgradeHeaders...)
underlyingConn, err := upgrader.Upgrade(w, req, resp.Header)
if err != nil {
ctx.log.Errorf("Unable to hijack the connection: %v %v", reflect.TypeOf(w), err)
ctx.errHandler.ServeHTTP(w, req, err)
ctx.log.Errorf("Error while upgrading connection : %v", err)
return
}
// it is now caller's responsibility to Close the underlying connection
defer underlyingConn.Close()
defer targetConn.Close()
ctx.log.Infof("Writing outgoing Websocket request to target connection: %+v", outReq)
// write the modified incoming request to the dialed connection
if err = outReq.Write(targetConn); err != nil {
ctx.log.Errorf("Unable to copy request to target: %v", err)
ctx.errHandler.ServeHTTP(w, req, err)
return
}
errc := make(chan error, 2)
replicate := func(dst io.Writer, src io.Reader) {
_, err := io.Copy(dst, src)
errc <- err
}
go replicate(targetConn, underlyingConn)
go replicate(underlyingConn, targetConn)
go replicate(targetConn.UnderlyingConn(), underlyingConn.UnderlyingConn())
// Try to read the first message
t, msg, err := targetConn.ReadMessage()
if err != nil {
ctx.log.Errorf("Couldn't read first message : %v", err)
} else {
underlyingConn.WriteMessage(t, msg)
}
go replicate(underlyingConn.UnderlyingConn(), targetConn.UnderlyingConn())
<-errc
}
// copyRequest makes a copy of the specified request.
@@ -316,20 +316,18 @@ func (f *websocketForwarder) copyRequest(req *http.Request, u *url.URL) (outReq
outReq.URL.Scheme = "ws"
}
if requestURI, err := url.ParseRequestURI(outReq.RequestURI); err == nil {
outReq.URL.Path = requestURI.Path
outReq.URL.RawQuery = requestURI.RawQuery
}
outReq.URL.Host = u.Host
outReq.URL.Opaque = req.RequestURI
// raw query is already included in RequestURI, so ignore it to avoid dupes
outReq.URL.RawQuery = ""
outReq.Proto = "HTTP/1.1"
outReq.ProtoMajor = 1
outReq.ProtoMinor = 1
// Overwrite close flag so we can keep persistent connection for the backend servers
outReq.Close = false
outReq.Header = make(http.Header)
//gorilla websocket use this header to set the request.Host tested in checkSameOrigin
outReq.Header.Set("Host", outReq.Host)
utils.CopyHeaders(outReq.Header, req.Header)
utils.RemoveHeaders(outReq.Header, WebsocketDialHeaders...)
if f.rewriter != nil {
f.rewriter.Rewrite(outReq)
@@ -351,3 +349,12 @@ func isWebsocketRequest(req *http.Request) bool {
}
return containsHeader(Connection, "upgrade") && containsHeader(Upgrade, "websocket")
}
func shallowCopyTrailers(dstHeader, srcTrailer http.Header, forceSetTrailers bool) {
for k, vv := range srcTrailer {
if forceSetTrailers {
k = http.TrailerPrefix + k
}
dstHeader[k] = vv
}
}

View File

@@ -1,20 +1,25 @@
package forward
const (
XForwardedProto = "X-Forwarded-Proto"
XForwardedFor = "X-Forwarded-For"
XForwardedHost = "X-Forwarded-Host"
XForwardedServer = "X-Forwarded-Server"
Connection = "Connection"
KeepAlive = "Keep-Alive"
ProxyAuthenticate = "Proxy-Authenticate"
ProxyAuthorization = "Proxy-Authorization"
Te = "Te" // canonicalized version of "TE"
Trailers = "Trailers"
TransferEncoding = "Transfer-Encoding"
Upgrade = "Upgrade"
ContentLength = "Content-Length"
ContentType = "Content-Type"
XForwardedProto = "X-Forwarded-Proto"
XForwardedFor = "X-Forwarded-For"
XForwardedHost = "X-Forwarded-Host"
XForwardedPort = "X-Forwarded-Port"
XForwardedServer = "X-Forwarded-Server"
Connection = "Connection"
KeepAlive = "Keep-Alive"
ProxyAuthenticate = "Proxy-Authenticate"
ProxyAuthorization = "Proxy-Authorization"
Te = "Te" // canonicalized version of "TE"
Trailers = "Trailers"
TransferEncoding = "Transfer-Encoding"
Upgrade = "Upgrade"
ContentLength = "Content-Length"
ContentType = "Content-Type"
SecWebsocketKey = "Sec-Websocket-Key"
SecWebsocketVersion = "Sec-Websocket-Version"
SecWebsocketExtensions = "Sec-Websocket-Extensions"
SecWebsocketAccept = "Sec-Websocket-Accept"
)
// Hop-by-hop headers. These are removed when sent to the backend.
@@ -30,3 +35,18 @@ var HopHeaders = []string{
TransferEncoding,
Upgrade,
}
var WebsocketDialHeaders = []string{
Upgrade,
Connection,
SecWebsocketKey,
SecWebsocketVersion,
SecWebsocketExtensions,
SecWebsocketAccept,
}
var WebsocketUpgradeHeaders = []string{
Upgrade,
Connection,
SecWebsocketAccept,
}

View File

@@ -32,6 +32,12 @@ func (rw *HeaderRewriter) Rewrite(req *http.Request) {
req.Header.Set(XForwardedProto, "http")
}
if xfp := req.Header.Get(XForwardedPort); xfp != "" && rw.TrustForwardHeader {
req.Header.Set(XForwardedPort, xfp)
} else if req.URL.Port() != "" {
req.Header.Set(XForwardedPort, req.URL.Port())
}
if xfh := req.Header.Get(XForwardedHost); xfh != "" && rw.TrustForwardHeader {
req.Header.Set(XForwardedHost, xfh)
} else if req.Host != "" {

View File

@@ -38,7 +38,7 @@ func (s *StickySession) GetBackend(req *http.Request, servers []*url.URL) (*url.
}
func (s *StickySession) StickBackend(backend *url.URL, w *http.ResponseWriter) {
c := &http.Cookie{Name: s.cookiename, Value: backend.String()}
c := &http.Cookie{Name: s.cookiename, Value: backend.String(), Path: "/"}
http.SetCookie(*w, c)
return
}

View File

@@ -7,7 +7,8 @@ RUN apt-get -yq update \
&& apt-get -yq --no-install-suggests --no-install-recommends --force-yes install apt-transport-https \
&& curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
&& echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
&& apt-get -yq update && apt-get -yq --no-install-suggests --no-install-recommends --force-yes install yarn
&& apt-get -yq update && apt-get -yq --no-install-suggests --no-install-recommends --force-yes install yarn \
&& rm -fr /var/lib/apt/lists/
COPY package.json $WEBUI_DIR/
COPY yarn.lock $WEBUI_DIR/

View File

@@ -12,34 +12,37 @@ angular
function Providers($resource, $q) {
const resourceProvider = $resource('../api/providers');
return {
get: function() {
get: function () {
return $q((resolve, reject) => {
resourceProvider.get().$promise.then((rawProviders) => {
for (let providerName in rawProviders) {
if (rawProviders.hasOwnProperty(providerName)) {
if (!providerName.startsWith('$')) {
// BackEnds mapping
let bckends = rawProviders[providerName].backends;
resourceProvider.get()
.$promise
.then((rawProviders) => {
for (let providerName in rawProviders) {
if (rawProviders.hasOwnProperty(providerName)) {
if (!providerName.startsWith('$')) {
// BackEnds mapping
let bckends = rawProviders[providerName].backends || {};
rawProviders[providerName].backends = Object.keys(bckends)
.map(key => {
const goodBackend = bckends[key];
goodBackend.backendId = key;
return goodBackend;
});
rawProviders[providerName].backends = Object.keys(bckends).map(key => {
const goodBackend = bckends[key];
goodBackend.backendId = key;
return goodBackend;
});
// FrontEnds mapping
let frtends = rawProviders[providerName].frontends;
rawProviders[providerName].frontends = Object.keys(frtends).map(key => {
const goodFrontend = frtends[key];
goodFrontend.frontendId = key;
return goodFrontend;
});
// FrontEnds mapping
let frtends = rawProviders[providerName].frontends || {};
rawProviders[providerName].frontends = Object.keys(frtends)
.map(key => {
const goodFrontend = frtends[key];
goodFrontend.frontendId = key;
return goodFrontend;
});
}
}
}
}
resolve(rawProviders);
}).catch(reject);
resolve(rawProviders);
})
.catch(reject);
});
}
};