Merge branch 'v1.7' into master
This commit is contained in:
commit
dad0e75121
38 changed files with 1171 additions and 235 deletions
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue