Compare commits

...

10 Commits

Author SHA1 Message Date
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
27 changed files with 551 additions and 115 deletions

View File

@@ -2,7 +2,7 @@
set -e
sudo -E apt-get -yq update
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-engine=${DOCKER_VERSION}*
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

View File

@@ -5,8 +5,6 @@ export secure='btt4r13t09gQlHb6gYrvGC2yGCMMHfnp1Mz1RQedc4Mpf/FfT8aE6xmK2a2i9CCvs
export REPO='containous/traefik'
export DOCKER_VERSION=1.12.6
if VERSION=$(git describe --exact-match --abbrev=0 --tags);
then
export VERSION

View File

@@ -11,7 +11,6 @@ env:
- VERSION: $TRAVIS_TAG
- CODENAME: raclette
- N_MAKE_JOBS: 2
- DOCKER_VERSION: 1.12.6
script:
- echo "Skipping tests... (Tests are executed on SemaphoreCI)"
@@ -21,7 +20,7 @@ before_deploy:
if ! [ "$BEFORE_DEPLOY_RUN" ]; then
export BEFORE_DEPLOY_RUN=1;
sudo -E apt-get -yq update;
sudo -E apt-get -yq --no-install-suggests --no-install-recommends --force-yes install docker-engine=${DOCKER_VERSION}*;
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;

View File

@@ -1,5 +1,15 @@
# Change Log
## [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)

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

10
glide.lock generated
View File

@@ -1,5 +1,5 @@
hash: e59e8244152a823cd3633fb09cdd583c4e5be78d7b50fb7047ba6b6a9ed5e8ec
updated: 2017-05-19T23:30:19.890844996+02:00
hash: cebc972cf87c4b0a8f86801f38750c51b09c8dee3bf62bb48f8eaa6ab7946352
updated: 2017-06-29T16: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: 15ea23e360abb8b25071e677aed344f31838e403
version: dd6cbd4c2d71294a19fb89158f2a00d427f174ab
- name: github.com/ghodss/yaml
version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee
- name: github.com/go-ini/ini
@@ -409,7 +409,7 @@ imports:
- name: github.com/vdemeester/docker-events
version: be74d4929ec1ad118df54349fda4b0cba60f849b
- name: github.com/vulcand/oxy
version: f88530866c561d24a6b5aac49f76d6351b788b9f
version: 7da864c1d53bd58165435bb78bbf8c01f01c8f4a
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: 7da864c1d53bd58165435bb78bbf8c01f01c8f4a
repo: https://github.com/containous/oxy.git
vcs: git
subpackages:
@@ -93,7 +93,7 @@ import:
- 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

@@ -263,7 +263,7 @@ func (c *clientImpl) WatchAll(labelSelector string, stopCh <-chan struct{}) (<-c
// fireEvent checks if all controllers have synced before firing
// Used after startup or a reconnect
func (c *clientImpl) fireEvent(event interface{}, eventCh chan interface{}) {
if !c.ingController.HasSynced() || !c.svcController.HasSynced() || !c.epController.HasSynced() {
if !c.ingController.HasSynced() || !c.svcController.HasSynced() || !c.epController.HasSynced() || !c.secController.HasSynced() {
return
}
eventHandlerFunc(eventCh, event)

View File

@@ -110,11 +110,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)
}
})
@@ -171,7 +171,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,
@@ -290,18 +291,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
}
@@ -314,9 +312,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
@@ -331,6 +329,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
}

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

@@ -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

@@ -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

@@ -150,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
@@ -190,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) {
@@ -298,8 +301,7 @@ func (r *marathonClient) apiCall(method, path string, body, result interface{})
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
@@ -317,7 +319,8 @@ func (r *marathonClient) apiCall(method, path string, body, result interface{})
}
}
// buildAPIRequest creates a default API request
// 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()
@@ -328,7 +331,7 @@ func (r *marathonClient) buildAPIRequest(method, path string, reader io.Reader)
// Build the HTTP request to Marathon
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
}

View File

@@ -209,7 +209,9 @@ func (r *marathonClient) WaitOnGroup(name string, timeout time.Duration) error {
func (r *marathonClient) DeleteGroup(name string, force bool) (*DeploymentID, error) {
version := new(DeploymentID)
path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
path = buildPathWithForceParam(path, force)
if force {
path += "?force=true"
}
if err := r.apiDelete(path, nil, version); err != nil {
return nil, err
}
@@ -224,7 +226,9 @@ func (r *marathonClient) DeleteGroup(name string, force bool) (*DeploymentID, er
func (r *marathonClient) UpdateGroup(name string, group *Group, force bool) (*DeploymentID, error) {
deploymentID := new(DeploymentID)
path := fmt.Sprintf("%s/%s", marathonAPIGroups, trimRootPath(name))
path = buildPathWithForceParam(path, force)
if force {
path += "?force=true"
}
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

@@ -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,40 +163,81 @@ 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

View File

@@ -16,12 +16,54 @@ 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

View File

@@ -4,6 +4,7 @@
package forward
import (
"bufio"
"crypto/tls"
"io"
"net"
@@ -158,7 +159,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 +171,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 +191,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 +218,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
@@ -290,14 +306,26 @@ func (f *websocketForwarder) serveHTTP(w http.ResponseWriter, req *http.Request,
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
br := bufio.NewReader(targetConn)
resp, err := http.ReadResponse(br, req)
resp.Write(underlyingConn)
defer resp.Body.Close()
// We connect the conn only if the switching protocol has not failed
if resp.StatusCode == http.StatusSwitchingProtocols {
ctx.log.Infof("Switching protocol success")
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)
<-errc
} else {
ctx.log.Infof("Switching protocol failed")
}
go replicate(targetConn, underlyingConn)
go replicate(underlyingConn, targetConn)
<-errc
}
// copyRequest makes a copy of the specified request.
@@ -351,3 +379,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
}
}