1
0
Fork 0

Merge branch 'v1.7' into master

This commit is contained in:
Fernandez Ludovic 2018-08-02 17:28:44 +02:00
commit dad0e75121
38 changed files with 1171 additions and 235 deletions

View file

@ -6,12 +6,14 @@ import (
"fmt"
"io/ioutil"
fmtlog "log"
"net/url"
"reflect"
"strings"
"sync"
"time"
"github.com/BurntSushi/ty/fun"
"github.com/cenk/backoff"
"github.com/containous/flaeg/parse"
"github.com/containous/traefik/log"
"github.com/containous/traefik/rules"
@ -73,6 +75,8 @@ type Certificate struct {
type DNSChallenge struct {
Provider string `description:"Use a DNS-01 based challenge provider rather than HTTPS."`
DelayBeforeCheck parse.Duration `description:"Assume DNS propagates after a delay in seconds rather than finding and querying nameservers."`
preCheckTimeout time.Duration
preCheckInterval time.Duration
}
// HTTPChallenge contains HTTP challenge Configuration
@ -130,7 +134,8 @@ func (p *Provider) Init(_ types.Constraints) error {
}
// Reset Account if caServer changed, thus registration URI can be updated
if p.account != nil && p.account.Registration != nil && !strings.HasPrefix(p.account.Registration.URI, p.CAServer) {
if p.account != nil && p.account.Registration != nil && !isAccountMatchingCaServer(p.account.Registration.URI, p.CAServer) {
log.Info("Account URI does not match the current CAServer. The account will be reset")
p.account = nil
}
@ -142,6 +147,20 @@ func (p *Provider) Init(_ types.Constraints) error {
return nil
}
func isAccountMatchingCaServer(accountURI string, serverURI string) bool {
aru, err := url.Parse(accountURI)
if err != nil {
log.Infof("Unable to parse account.Registration URL : %v", err)
return false
}
cau, err := url.Parse(serverURI)
if err != nil {
log.Infof("Unable to parse CAServer URL : %v", err)
return false
}
return cau.Hostname() == aru.Hostname()
}
// Provide allows the file provider to provide configurations to traefik
// using the given Configuration channel.
func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool) error {
@ -246,6 +265,16 @@ func (p *Provider) getClient() (*acme.Client, error) {
if err != nil {
return nil, err
}
// Same default values than LEGO
p.DNSChallenge.preCheckTimeout = 60 * time.Second
p.DNSChallenge.preCheckInterval = 2 * time.Second
// Set the precheck timeout into the DNSChallenge provider
if challengeProviderTimeout, ok := provider.(acme.ChallengeProviderTimeout); ok {
p.DNSChallenge.preCheckTimeout, p.DNSChallenge.preCheckInterval = challengeProviderTimeout.Timeout()
}
} else if p.HTTPChallenge != nil && len(p.HTTPChallenge.EntryPoint) > 0 {
log.Debug("Using HTTP Challenge provider.")
@ -345,13 +374,20 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati
return nil, fmt.Errorf("cannot get ACME client %v", err)
}
var certificate *acme.CertificateResource
bundle := true
certificate, err := client.ObtainCertificate(uncheckedDomains, bundle, nil, OSCPMustStaple)
if err != nil {
return nil, fmt.Errorf("cannot obtain certificates: %+v", err)
if p.useCertificateWithRetry(uncheckedDomains) {
certificate, err = obtainCertificateWithRetry(domains, client, p.DNSChallenge.preCheckTimeout, p.DNSChallenge.preCheckInterval, bundle)
} else {
certificate, err = client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple)
}
if err != nil {
return nil, fmt.Errorf("unable to generate a certificate for the domains %v: %v", uncheckedDomains, err)
}
if certificate == nil {
return nil, fmt.Errorf("domains %v do not generate a certificate", uncheckedDomains)
}
if len(certificate.Certificate) == 0 || len(certificate.PrivateKey) == 0 {
return nil, fmt.Errorf("domains %v generate certificate with no value: %v", uncheckedDomains, certificate)
}
@ -368,6 +404,60 @@ func (p *Provider) resolveCertificate(domain types.Domain, domainFromConfigurati
return certificate, nil
}
func (p *Provider) useCertificateWithRetry(domains []string) bool {
// Check if we can use the retry mechanism only if we use the DNS Challenge and if is there are at least 2 domains to check
if p.DNSChallenge != nil && len(domains) > 1 {
rootDomain := ""
for _, searchWildcardDomain := range domains {
// Search a wildcard domain if not already found
if len(rootDomain) == 0 && strings.HasPrefix(searchWildcardDomain, "*.") {
rootDomain = strings.TrimPrefix(searchWildcardDomain, "*.")
if len(rootDomain) > 0 {
// Look for a root domain which matches the wildcard domain
for _, searchRootDomain := range domains {
if rootDomain == searchRootDomain {
// If the domains list contains a wildcard domain and its root domain, we can use the retry mechanism to obtain the certificate
return true
}
}
}
// There is only one wildcard domain in the slice, if its root domain has not been found, the retry mechanism does not have to be used
return false
}
}
}
return false
}
func obtainCertificateWithRetry(domains []string, client *acme.Client, timeout, interval time.Duration, bundle bool) (*acme.CertificateResource, error) {
var certificate *acme.CertificateResource
var err error
operation := func() error {
certificate, err = client.ObtainCertificate(domains, bundle, nil, OSCPMustStaple)
return err
}
notify := func(err error, time time.Duration) {
log.Errorf("Error obtaining certificate retrying in %s", time)
}
// Define a retry backOff to let LEGO tries twice to obtain a certificate for both wildcard and root domain
ebo := backoff.NewExponentialBackOff()
ebo.MaxElapsedTime = 2 * timeout
ebo.MaxInterval = interval
rbo := backoff.WithMaxRetries(ebo, 2)
err = backoff.RetryNotify(safe.OperationWithRecover(operation), rbo, notify)
if err != nil {
log.Errorf("Error obtaining certificate: %v", err)
return nil, err
}
return certificate, nil
}
func dnsOverrideDelay(delay parse.Duration) error {
if delay == 0 {
return nil

View file

@ -429,3 +429,136 @@ func TestDeleteUnnecessaryDomains(t *testing.T) {
})
}
}
func TestIsAccountMatchingCaServer(t *testing.T) {
testCases := []struct {
desc string
accountURI string
serverURI string
expected bool
}{
{
desc: "acme staging with matching account",
accountURI: "https://acme-staging-v02.api.letsencrypt.org/acme/acct/1234567",
serverURI: "https://acme-staging-v02.api.letsencrypt.org/acme/directory",
expected: true,
},
{
desc: "acme production with matching account",
accountURI: "https://acme-v02.api.letsencrypt.org/acme/acct/1234567",
serverURI: "https://acme-v02.api.letsencrypt.org/acme/directory",
expected: true,
},
{
desc: "http only acme with matching account",
accountURI: "http://acme.api.letsencrypt.org/acme/acct/1234567",
serverURI: "http://acme.api.letsencrypt.org/acme/directory",
expected: true,
},
{
desc: "different subdomains for account and server",
accountURI: "https://test1.example.org/acme/acct/1234567",
serverURI: "https://test2.example.org/acme/directory",
expected: false,
},
{
desc: "different domains for account and server",
accountURI: "https://test.example1.org/acme/acct/1234567",
serverURI: "https://test.example2.org/acme/directory",
expected: false,
},
{
desc: "different tld for account and server",
accountURI: "https://test.example.com/acme/acct/1234567",
serverURI: "https://test.example.org/acme/directory",
expected: false,
},
{
desc: "malformed account url",
accountURI: "//|\\/test.example.com/acme/acct/1234567",
serverURI: "https://test.example.com/acme/directory",
expected: false,
},
{
desc: "malformed server url",
accountURI: "https://test.example.com/acme/acct/1234567",
serverURI: "//|\\/test.example.com/acme/directory",
expected: false,
},
{
desc: "malformed server and account url",
accountURI: "//|\\/test.example.com/acme/acct/1234567",
serverURI: "//|\\/test.example.com/acme/directory",
expected: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
result := isAccountMatchingCaServer(test.accountURI, test.serverURI)
assert.Equal(t, test.expected, result)
})
}
}
func TestUseBackOffToObtainCertificate(t *testing.T) {
testCases := []struct {
desc string
domains []string
dnsChallenge *DNSChallenge
expectedResponse bool
}{
{
desc: "only one single domain",
domains: []string{"acme.wtf"},
dnsChallenge: &DNSChallenge{},
expectedResponse: false,
},
{
desc: "only one wildcard domain",
domains: []string{"*.acme.wtf"},
dnsChallenge: &DNSChallenge{},
expectedResponse: false,
},
{
desc: "wildcard domain with no root domain",
domains: []string{"*.acme.wtf", "foo.acme.wtf", "bar.acme.wtf", "foo.bar"},
dnsChallenge: &DNSChallenge{},
expectedResponse: false,
},
{
desc: "wildcard and root domain",
domains: []string{"*.acme.wtf", "foo.acme.wtf", "bar.acme.wtf", "acme.wtf"},
dnsChallenge: &DNSChallenge{},
expectedResponse: true,
},
{
desc: "wildcard and root domain but no DNS challenge",
domains: []string{"*.acme.wtf", "acme.wtf"},
dnsChallenge: nil,
expectedResponse: false,
},
{
desc: "two wildcard domains (must never happen)",
domains: []string{"*.acme.wtf", "*.bar.foo"},
dnsChallenge: nil,
expectedResponse: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
acmeProvider := Provider{Configuration: &Configuration{DNSChallenge: test.dnsChallenge}}
actualResponse := acmeProvider.useCertificateWithRetry(test.domains)
assert.Equal(t, test.expectedResponse, actualResponse, "unexpected response to use backOff")
})
}
}

View file

@ -63,6 +63,7 @@ const (
annotationKubernetesPublicKey = "ingress.kubernetes.io/public-key"
annotationKubernetesReferrerPolicy = "ingress.kubernetes.io/referrer-policy"
annotationKubernetesIsDevelopment = "ingress.kubernetes.io/is-development"
annotationKubernetesProtocol = "ingress.kubernetes.io/protocol"
)
// TODO [breaking] remove label support

View file

@ -43,6 +43,8 @@ const (
traefikDefaultIngressClass = "traefik"
defaultBackendName = "global-default-backend"
defaultFrontendName = "global-default-frontend"
allowedProtocolHTTPS = "https"
allowedProtocolH2C = "h2c"
)
// IngressEndpoint holds the endpoint information for the Kubernetes provider
@ -312,6 +314,16 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
protocol = "https"
}
protocol = getStringValue(i.Annotations, annotationKubernetesProtocol, protocol)
switch protocol {
case allowedProtocolHTTPS:
case allowedProtocolH2C:
case label.DefaultProtocol:
default:
log.Errorf("Invalid protocol %s/%s specified for Ingress %s - skipping", annotationKubernetesProtocol, i.Namespace, i.Name)
continue
}
if service.Spec.Type == "ExternalName" {
url := protocol + "://" + service.Spec.ExternalName
if port.Port != 443 && port.Port != 80 {
@ -535,7 +547,7 @@ func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.I
if ruleType == ruleTypeReplacePath {
return "", fmt.Errorf("rewrite-target must not be used together with annotation %q", annotationKubernetesRuleType)
}
rewriteTargetRule := fmt.Sprintf("ReplacePathRegex: ^%s/(.*) %s/$1", pa.Path, strings.TrimRight(rewriteTarget, "/"))
rewriteTargetRule := fmt.Sprintf("ReplacePathRegex: ^%s(.*) %s$1", pa.Path, strings.TrimRight(rewriteTarget, "/"))
rules = append(rules, rewriteTargetRule)
}

View file

@ -1222,6 +1222,41 @@ rateset:
iPaths(onePath(iPath("/customheaders"), iBackend("service1", intstr.FromInt(80))))),
),
),
buildIngress(
iNamespace("testing"),
iAnnotation(annotationKubernetesProtocol, "h2c"),
iRules(
iRule(
iHost("protocol"),
iPaths(onePath(iPath("/valid"), iBackend("service1", intstr.FromInt(80))))),
),
),
buildIngress(
iNamespace("testing"),
iAnnotation(annotationKubernetesProtocol, "foobar"),
iRules(
iRule(
iHost("protocol"),
iPaths(onePath(iPath("/notvalid"), iBackend("service1", intstr.FromInt(80))))),
),
),
buildIngress(
iNamespace("testing"),
iAnnotation(annotationKubernetesProtocol, "http"),
iRules(
iRule(
iHost("protocol"),
iPaths(onePath(iPath("/missmatch"), iBackend("serviceHTTPS", intstr.FromInt(443))))),
),
),
buildIngress(
iNamespace("testing"),
iRules(
iRule(
iHost("protocol"),
iPaths(onePath(iPath("/noAnnotation"), iBackend("serviceHTTPS", intstr.FromInt(443))))),
),
),
}
services := []*corev1.Service{
@ -1243,6 +1278,16 @@ rateset:
clusterIP("10.0.0.2"),
sPorts(sPort(802, ""))),
),
buildService(
sName("serviceHTTPS"),
sNamespace("testing"),
sUID("2"),
sSpec(
clusterIP("10.0.0.3"),
sType("ExternalName"),
sExternalName("example.com"),
sPorts(sPort(443, "https"))),
),
}
secrets := []*corev1.Secret{
@ -1350,6 +1395,28 @@ rateset:
servers(),
lbMethod("wrr"),
),
backend("protocol/valid",
servers(
server("h2c://example.com", weight(1)),
server("h2c://example.com", weight(1))),
lbMethod("wrr"),
),
backend("protocol/notvalid",
servers(),
lbMethod("wrr"),
),
backend("protocol/missmatch",
servers(
server("http://example.com", weight(1)),
server("http://example.com", weight(1))),
lbMethod("wrr"),
),
backend("protocol/noAnnotation",
servers(
server("https://example.com", weight(1)),
server("https://example.com", weight(1))),
lbMethod("wrr"),
),
),
frontends(
frontend("foo/bar",
@ -1408,7 +1475,7 @@ rateset:
frontend("rewrite/api",
passHostHeader(),
routes(
route("/api", "PathPrefix:/api;ReplacePathRegex: ^/api/(.*) /$1"),
route("/api", "PathPrefix:/api;ReplacePathRegex: ^/api(.*) $1"),
route("rewrite", "Host:rewrite")),
),
frontend("error-pages/errorpages",
@ -1481,6 +1548,34 @@ rateset:
route("root", "Host:root"),
),
),
frontend("protocol/valid",
passHostHeader(),
routes(
route("/valid", "PathPrefix:/valid"),
route("protocol", "Host:protocol"),
),
),
frontend("protocol/notvalid",
passHostHeader(),
routes(
route("/notvalid", "PathPrefix:/notvalid"),
route("protocol", "Host:protocol"),
),
),
frontend("protocol/missmatch",
passHostHeader(),
routes(
route("/missmatch", "PathPrefix:/missmatch"),
route("protocol", "Host:protocol"),
),
),
frontend("protocol/noAnnotation",
passHostHeader(),
routes(
route("/noAnnotation", "PathPrefix:/noAnnotation"),
route("protocol", "Host:protocol"),
),
),
),
)