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 8f3d1d01a..01c345e57 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 329ebcde5..b11dd867c 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -1571,6 +1571,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( @@ -3476,3 +3484,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") +}