diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index caada86a0..2883d5cf7 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -187,12 +187,13 @@ func runCmd(globalConfiguration *configuration.GlobalConfiguration, configFile s providerAggregator := configuration.NewProviderAggregator(globalConfiguration) - acmeprovider := globalConfiguration.InitACMEProvider() - if acmeprovider != nil { - - if err := providerAggregator.AddProvider(acmeprovider); err != nil { - log.Errorf("Error initializing provider ACME: %v", err) - acmeprovider = nil + acmeProvider, err := globalConfiguration.InitACMEProvider() + if err != nil { + log.Errorf("Unable to initialize ACME provider: %v", err) + } else if acmeProvider != nil { + if err := providerAggregator.AddProvider(acmeProvider); err != nil { + log.Errorf("Unable to add ACME provider to the providers list: %v", err) + acmeProvider = nil } } @@ -204,23 +205,23 @@ func runCmd(globalConfiguration *configuration.GlobalConfiguration, configFile s } internalRouter := router.NewInternalRouterAggregator(*globalConfiguration, entryPointName) - if acmeprovider != nil { - if acmeprovider.HTTPChallenge != nil && entryPointName == acmeprovider.HTTPChallenge.EntryPoint { - internalRouter.AddRouter(acmeprovider) + if acmeProvider != nil { + if acmeProvider.HTTPChallenge != nil && entryPointName == acmeProvider.HTTPChallenge.EntryPoint { + internalRouter.AddRouter(acmeProvider) } // TLS ALPN 01 - if acmeprovider.TLSChallenge != nil && acmeprovider.HTTPChallenge == nil && acmeprovider.DNSChallenge == nil { - entryPoint.TLSALPNGetter = acmeprovider.GetTLSALPNCertificate + if acmeProvider.TLSChallenge != nil && acmeProvider.HTTPChallenge == nil && acmeProvider.DNSChallenge == nil { + entryPoint.TLSALPNGetter = acmeProvider.GetTLSALPNCertificate } - if acmeprovider.OnDemand && entryPointName == acmeprovider.EntryPoint { - entryPoint.OnDemandListener = acmeprovider.ListenRequest + if acmeProvider.OnDemand && entryPointName == acmeProvider.EntryPoint { + entryPoint.OnDemandListener = acmeProvider.ListenRequest } - if entryPointName == acmeprovider.EntryPoint { + if entryPointName == acmeProvider.EntryPoint { entryPoint.CertificateStore = traefiktls.NewCertificateStore() - acmeprovider.SetCertificateStore(entryPoint.CertificateStore) + acmeProvider.SetCertificateStore(entryPoint.CertificateStore) log.Debugf("Setting Acme Certificate store from Entrypoint: %s", entryPointName) } } @@ -230,9 +231,9 @@ func runCmd(globalConfiguration *configuration.GlobalConfiguration, configFile s } svr := server.NewServer(*globalConfiguration, providerAggregator, entryPoints) - if acmeprovider != nil && acmeprovider.OnHostRule { - acmeprovider.SetConfigListenerChan(make(chan types.Configuration)) - svr.AddListener(acmeprovider.ListenConfiguration) + if acmeProvider != nil && acmeProvider.OnHostRule { + acmeProvider.SetConfigListenerChan(make(chan types.Configuration)) + svr.AddListener(acmeProvider.ListenConfiguration) } ctx := cmd.ContextWithSignal(context.Background()) diff --git a/configuration/configuration.go b/configuration/configuration.go index 2323e7d0d..b20075042 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -33,6 +33,7 @@ import ( "github.com/containous/traefik/provider/zk" "github.com/containous/traefik/tls" "github.com/containous/traefik/types" + "github.com/pkg/errors" ) const ( @@ -271,8 +272,13 @@ func (gc *GlobalConfiguration) initACMEProvider() { } // InitACMEProvider create an acme provider from the ACME part of globalConfiguration -func (gc *GlobalConfiguration) InitACMEProvider() *acmeprovider.Provider { +func (gc *GlobalConfiguration) InitACMEProvider() (*acmeprovider.Provider, error) { if gc.ACME != nil { + if len(gc.ACME.Storage) == 0 { + // Delete the ACME configuration to avoid starting ACME in cluster mode + gc.ACME = nil + return nil, errors.New("unable to initialize ACME provider with no storage location for the certificates") + } // TODO: Remove when Provider ACME will replace totally ACME // If provider file, use Provider ACME instead of ACME if gc.Cluster == nil { @@ -296,10 +302,10 @@ func (gc *GlobalConfiguration) InitACMEProvider() *acmeprovider.Provider { provider.Store = store acme.ConvertToNewFormat(provider.Storage) gc.ACME = nil - return provider + return provider, nil } } - return nil + return nil, nil } func getSafeACMECAServer(caServerSrc string) string { diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 97a0a9fa9..f41a73fbd 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -3,10 +3,12 @@ package configuration import ( "testing" + "github.com/containous/traefik/acme" "github.com/containous/traefik/middlewares/tracing" "github.com/containous/traefik/middlewares/tracing/jaeger" "github.com/containous/traefik/middlewares/tracing/zipkin" "github.com/containous/traefik/provider" + acmeprovider "github.com/containous/traefik/provider/acme" "github.com/containous/traefik/provider/file" "github.com/stretchr/testify/assert" ) @@ -171,3 +173,52 @@ func TestSetEffectiveConfigurationTracing(t *testing.T) { }) } } + +func TestInitACMEProvider(t *testing.T) { + testCases := []struct { + desc string + acmeConfiguration *acme.ACME + expectedConfiguration *acmeprovider.Provider + noError bool + }{ + { + desc: "No ACME configuration", + acmeConfiguration: nil, + expectedConfiguration: nil, + noError: true, + }, + { + desc: "ACME configuration with storage", + acmeConfiguration: &acme.ACME{Storage: "foo/acme.json"}, + expectedConfiguration: &acmeprovider.Provider{Configuration: &acmeprovider.Configuration{Storage: "foo/acme.json"}}, + noError: true, + }, + { + desc: "ACME configuration with no storage", + acmeConfiguration: &acme.ACME{}, + expectedConfiguration: nil, + noError: false, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + gc := &GlobalConfiguration{ + ACME: test.acmeConfiguration, + } + + configuration, err := gc.InitACMEProvider() + + assert.True(t, (err == nil) == test.noError) + + if test.expectedConfiguration == nil { + assert.Nil(t, configuration) + } else { + assert.Equal(t, test.expectedConfiguration.Storage, configuration.Storage) + } + }) + } +} diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index c194af91e..e08e796c7 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -742,6 +742,45 @@ You should now be able to visit the websites in your browser. - [cheeses.minikube/cheddar](http://cheeses.minikube/cheddar/) - [cheeses.minikube/wensleydale](http://cheeses.minikube/wensleydale/) +## Multiple Ingress Definitions for the Same Host (or Host+Path) + +Træfik will merge multiple Ingress definitions for the same host/path pair into one definition. + +Let's say the number of cheese services is growing. +It is now time to move the cheese services to a dedicated cheese namespace to simplify the managements of cheese and non-cheese services. + +Simply deploy a new Ingress Object with the same host an path into the cheese namespace: + +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: cheese + namespace: cheese + annotations: + kubernetes.io/ingress.class: traefik + traefik.frontend.rule.type: PathPrefixStrip +spec: + rules: + - host: cheese.minikube + http: + paths: + - path: /cheddar + backend: + serviceName: cheddar + servicePort: http +``` + +Træfik will now look for cheddar service endpoints (ports on healthy pods) in both the cheese and the default namespace. +Deploying cheddar into the cheese namespace and afterwards shutting down cheddar in the default namespace is enough to migrate the traffic. + +!!! note + The kubernetes documentation does not specify this merging behavior. + +!!! note + Merging ingress definitions can cause problems if the annotations differ or if the services handle requests differently. + Be careful and extra cautious when running multiple overlapping ingress definitions. + ## Specifying Routing Priorities Sometimes you need to specify priority for ingress routes, especially when handling wildcard routes. diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 01a6b4429..50f3b30f9 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -258,7 +258,10 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) continue } - if _, exists := templateObjects.Frontends[baseName]; !exists { + var frontend *types.Frontend + if fe, exists := templateObjects.Frontends[baseName]; exists { + frontend = fe + } else { auth, err := getAuthConfig(i, k8sClient) if err != nil { log.Errorf("Failed to retrieve auth configuration for ingress %s/%s: %s", i.Namespace, i.Name, err) @@ -269,7 +272,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints) - templateObjects.Frontends[baseName] = &types.Frontend{ + frontend = &types.Frontend{ Backend: baseName, PassHostHeader: passHostHeader, PassTLSCert: passTLSCert, @@ -285,26 +288,6 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) } } - if len(r.Host) > 0 { - if _, exists := templateObjects.Frontends[baseName].Routes[r.Host]; !exists { - templateObjects.Frontends[baseName].Routes[r.Host] = types.Route{ - Rule: getRuleForHost(r.Host), - } - } - } - - rule, err := getRuleForPath(pa, i) - if err != nil { - log.Errorf("Failed to get rule for ingress %s/%s: %s", i.Namespace, i.Name, err) - delete(templateObjects.Frontends, baseName) - continue - } - if rule != "" { - templateObjects.Frontends[baseName].Routes[pa.Path] = types.Route{ - Rule: rule, - } - } - service, exists, err := k8sClient.GetService(i.Namespace, pa.Backend.ServiceName) if err != nil { log.Errorf("Error while retrieving service information from k8s API %s/%s: %v", i.Namespace, pa.Backend.ServiceName, err) @@ -313,10 +296,30 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) if !exists { log.Errorf("Service not found for %s/%s", i.Namespace, pa.Backend.ServiceName) - delete(templateObjects.Frontends, baseName) continue } + rule, err := getRuleForPath(pa, i) + if err != nil { + log.Errorf("Failed to get rule for ingress %s/%s: %s", i.Namespace, i.Name, err) + continue + } + + if rule != "" { + frontend.Routes[pa.Path] = types.Route{ + Rule: rule, + } + } + + if len(r.Host) > 0 { + if _, exists := frontend.Routes[r.Host]; !exists { + frontend.Routes[r.Host] = types.Route{ + Rule: getRuleForHost(r.Host), + } + } + } + + templateObjects.Frontends[baseName] = frontend templateObjects.Backends[baseName].CircuitBreaker = getCircuitBreaker(service) templateObjects.Backends[baseName].LoadBalancer = getLoadBalancer(service) templateObjects.Backends[baseName].MaxConn = getMaxConn(service) diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index 9d4ea87b6..233187ae3 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -1597,6 +1597,14 @@ rateset: route("root", "Host:root"), ), ), + frontend("root2/", + passHostHeader(), + redirectRegex("root2/$", "root2/root2"), + routes( + route("/", "PathPrefix:/;ReplacePathRegex: ^/(.*) /abc$1"), + route("root2", "Host:root2"), + ), + ), frontend("root/root1", passHostHeader(), routes( @@ -3502,3 +3510,93 @@ func TestTemplateBreakingIngresssValues(t *testing.T) { assert.Equal(t, expected, actual) } + +func TestDivergingIngressDefinitions(t *testing.T) { + ingresses := []*extensionsv1beta1.Ingress{ + buildIngress( + iNamespace("testing"), + iRules( + iRule( + iHost("host-a"), + iPaths( + onePath(iBackend("service1", intstr.FromString("80"))), + )), + ), + ), + buildIngress( + iNamespace("testing"), + iRules( + iRule( + iHost("host-a"), + iPaths( + onePath(iBackend("missing", intstr.FromString("80"))), + )), + ), + ), + } + + services := []*corev1.Service{ + buildService( + sName("service1"), + sNamespace("testing"), + sUID("1"), + sSpec( + clusterIP("10.0.0.1"), + sPorts(sPort(80, "http")), + ), + ), + } + + endpoints := []*corev1.Endpoints{ + buildEndpoint( + eNamespace("testing"), + eName("service1"), + eUID("1"), + subset( + eAddresses( + eAddress("10.10.0.1"), + ), + ePorts(ePort(80, "http")), + ), + subset( + eAddresses( + eAddress("10.10.0.2"), + ), + ePorts(ePort(80, "http")), + ), + ), + } + + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + endpoints: endpoints, + watchChan: watchChan, + } + provider := Provider{} + + actual, err := provider.loadIngresses(client) + require.NoError(t, err, "error loading ingresses") + + expected := buildConfiguration( + backends( + backend("host-a", + servers( + server("http://10.10.0.1:80", weight(1)), + server("http://10.10.0.2:80", weight(1)), + ), + lbMethod("wrr"), + ), + ), + frontends( + frontend("host-a", + passHostHeader(), + routes( + route("host-a", "Host:host-a")), + ), + ), + ) + + assert.Equal(t, expected, actual, "error merging multiple backends") +}