From 461ebf6d8846ca94445d8e7009a97c418d2d1c3d Mon Sep 17 00:00:00 2001 From: Daniel Tomcej Date: Tue, 3 Jul 2018 10:58:03 -0600 Subject: [PATCH] Create Global Backend Ingress --- docs/configuration/backends/kubernetes.md | 22 ++++ examples/k8s/cheese-default-ingress.yaml | 8 ++ provider/kubernetes/builder_ingress_test.go | 27 ++++ provider/kubernetes/kubernetes.go | 108 ++++++++++++++- provider/kubernetes/kubernetes_test.go | 137 +++++++++++++++++++- 5 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 examples/k8s/cheese-default-ingress.yaml diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 071168030..3a49d97ea 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -324,3 +324,25 @@ More information are available in the [User Guide](/user-guide/kubernetes/#add- !!! note Only TLS certificates provided by users can be stored in Kubernetes Secrets. [Let's Encrypt](https://letsencrypt.org) certificates cannot be managed in Kubernets Secrets yet. + +### Global Default Backend Ingresses + +Ingresses can be created that look like the following: + +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: cheese +spec: + backend: + serviceName: stilton + servicePort: 80 +``` + +This ingress follows the [Global Default Backend](https://kubernetes.io/docs/concepts/services-networking/ingress/#the-ingress-resource) property of ingresses. +This will allow users to create a "default backend" that will match all unmatched requests. + +!!! note + Due to Træfik's use of priorities, you may have to set this ingress priority lower than other ingresses in your environment, to avoid this global ingress from satisfying requests that _could_ match other ingresses. + To do this, use the `traefik.ingress.kubernetes.io/priority` annotation (as seen in [General Annotations](/configuration/backends/kubernetes/#general-annotations)) on your ingresses accordingly. diff --git a/examples/k8s/cheese-default-ingress.yaml b/examples/k8s/cheese-default-ingress.yaml new file mode 100644 index 000000000..3c0d4f010 --- /dev/null +++ b/examples/k8s/cheese-default-ingress.yaml @@ -0,0 +1,8 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: cheese-default +spec: + backend: + serviceName: stilton + servicePort: 80 diff --git a/provider/kubernetes/builder_ingress_test.go b/provider/kubernetes/builder_ingress_test.go index fab3bd887..e6b0dbe91 100644 --- a/provider/kubernetes/builder_ingress_test.go +++ b/provider/kubernetes/builder_ingress_test.go @@ -42,6 +42,33 @@ func iRules(opts ...func(*extensionsv1beta1.IngressSpec)) func(*extensionsv1beta } } +func iSpecBackends(opts ...func(*extensionsv1beta1.IngressSpec)) func(*extensionsv1beta1.Ingress) { + return func(i *extensionsv1beta1.Ingress) { + s := &extensionsv1beta1.IngressSpec{} + for _, opt := range opts { + opt(s) + } + i.Spec = *s + } +} + +func iSpecBackend(opts ...func(*extensionsv1beta1.IngressBackend)) func(*extensionsv1beta1.IngressSpec) { + return func(s *extensionsv1beta1.IngressSpec) { + p := &extensionsv1beta1.IngressBackend{} + for _, opt := range opts { + opt(p) + } + s.Backend = p + } +} + +func iIngressBackend(name string, port intstr.IntOrString) func(*extensionsv1beta1.IngressBackend) { + return func(p *extensionsv1beta1.IngressBackend) { + p.ServiceName = name + p.ServicePort = port + } +} + func iRule(opts ...func(*extensionsv1beta1.IngressRule)) func(*extensionsv1beta1.IngressSpec) { return func(spec *extensionsv1beta1.IngressSpec) { r := &extensionsv1beta1.IngressRule{} diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 3d38a623a..d12fd3185 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -36,6 +36,8 @@ const ( ruleTypeReplacePath = "ReplacePath" traefikDefaultRealm = "traefik" traefikDefaultIngressClass = "traefik" + defaultBackendName = "global-default-backend" + defaultFrontendName = "global-default-frontend" ) // IngressEndpoint holds the endpoint information for the Kubernetes provider @@ -164,7 +166,7 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) { ingresses := k8sClient.GetIngresses() - templateObjects := types.Configuration{ + templateObjects := &types.Configuration{ Backends: map[string]*types.Backend{}, Frontends: map[string]*types.Frontend{}, } @@ -184,6 +186,14 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) } templateObjects.TLS = append(templateObjects.TLS, tlsSection...) + if i.Spec.Backend != nil { + err := p.addGlobalBackend(k8sClient, i, templateObjects) + if err != nil { + log.Errorf("Error creating global backend for ingress %s/%s: %v", i.Namespace, i.Name, err) + continue + } + } + var weightAllocator weightAllocator = &defaultWeightAllocator{} annotationPercentageWeights := getAnnotationName(i.Annotations, annotationKubernetesServiceWeights) if _, ok := i.Annotations[annotationPercentageWeights]; ok { @@ -351,7 +361,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) log.Errorf("Cannot update Ingress %s/%s due to error: %v", i.Namespace, i.Name, err) } } - return &templateObjects, nil + return templateObjects, nil } func (p *Provider) updateIngressStatus(i *extensionsv1beta1.Ingress, k8sClient Client) error { @@ -401,6 +411,100 @@ func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Config return configuration } +func (p *Provider) addGlobalBackend(cl Client, i *extensionsv1beta1.Ingress, templateObjects *types.Configuration) error { + // Ensure that we are not duplicating the frontend + if _, exists := templateObjects.Frontends[defaultFrontendName]; exists { + return errors.New("duplicate frontend: " + defaultFrontendName) + } + + // Ensure we are not duplicating the backend + if _, exists := templateObjects.Backends[defaultBackendName]; exists { + return errors.New("duplicate backend: " + defaultBackendName) + } + + templateObjects.Backends[defaultBackendName] = &types.Backend{ + Servers: make(map[string]types.Server), + LoadBalancer: &types.LoadBalancer{ + Method: "wrr", + }, + } + + service, exists, err := cl.GetService(i.Namespace, i.Spec.Backend.ServiceName) + if err != nil { + return fmt.Errorf("error while retrieving service information from k8s API %s/%s: %v", i.Namespace, i.Spec.Backend.ServiceName, err) + } + if !exists { + return fmt.Errorf("service not found for %s/%s", i.Namespace, i.Spec.Backend.ServiceName) + } + + templateObjects.Backends[defaultBackendName].CircuitBreaker = getCircuitBreaker(service) + templateObjects.Backends[defaultBackendName].LoadBalancer = getLoadBalancer(service) + templateObjects.Backends[defaultBackendName].MaxConn = getMaxConn(service) + templateObjects.Backends[defaultBackendName].Buffering = getBuffering(service) + + endpoints, exists, err := cl.GetEndpoints(service.Namespace, service.Name) + if err != nil { + return fmt.Errorf("error retrieving endpoint information from k8s API %s/%s: %v", service.Namespace, service.Name, err) + } + if !exists { + return fmt.Errorf("endpoints not found for %s/%s", service.Namespace, service.Name) + } + if len(endpoints.Subsets) == 0 { + return fmt.Errorf("endpoints not available for %s/%s", service.Namespace, service.Name) + } + + for _, subset := range endpoints.Subsets { + endpointPort := endpointPortNumber(corev1.ServicePort{Protocol: "TCP", Port: int32(i.Spec.Backend.ServicePort.IntValue())}, subset.Ports) + if endpointPort == 0 { + // endpoint port does not match service. + continue + } + + protocol := "http" + for _, address := range subset.Addresses { + if endpointPort == 443 || strings.HasPrefix(i.Spec.Backend.ServicePort.String(), "https") { + protocol = "https" + } + + url := fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(address.IP, strconv.FormatInt(int64(endpointPort), 10))) + name := url + if address.TargetRef != nil && address.TargetRef.Name != "" { + name = address.TargetRef.Name + } + + templateObjects.Backends[defaultBackendName].Servers[name] = types.Server{ + URL: url, + Weight: label.DefaultWeight, + } + } + } + + passHostHeader := getBoolValue(i.Annotations, annotationKubernetesPreserveHost, !p.DisablePassHostHeaders) + passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) + priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0) + entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints) + + templateObjects.Frontends[defaultFrontendName] = &types.Frontend{ + Backend: defaultBackendName, + PassHostHeader: passHostHeader, + PassTLSCert: passTLSCert, + Routes: make(map[string]types.Route), + Priority: priority, + WhiteList: getWhiteList(i), + Redirect: getFrontendRedirect(i), + EntryPoints: entryPoints, + Headers: getHeader(i), + Errors: getErrorPages(i), + RateLimit: getRateLimit(i), + } + + templateObjects.Frontends[defaultFrontendName].Routes["/"] = types.Route{ + Rule: "PathPrefix:/", + } + + return nil +} + func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.Ingress) (string, error) { if len(pa.Path) == 0 { return "", nil diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index 1b6a0afa8..037283b67 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -221,6 +221,134 @@ func TestLoadIngresses(t *testing.T) { assert.Equal(t, expected, actual) } +func TestLoadGlobalIngressWithPortNumbers(t *testing.T) { + ingresses := []*extensionsv1beta1.Ingress{ + buildIngress( + iNamespace("testing"), + iSpecBackends(iSpecBackend(iIngressBackend("service1", intstr.FromInt(80)))), + ), + } + + services := []*corev1.Service{ + buildService( + sName("service1"), + sNamespace("testing"), + sUID("1"), + sSpec( + clusterIP("10.0.0.1"), + sPorts(sPort(80, ""))), + ), + } + + endpoints := []*corev1.Endpoints{ + buildEndpoint( + eNamespace("testing"), + eName("service1"), + eUID("1"), + subset( + eAddresses(eAddress("10.10.0.1")), + ePorts(ePort(8080, ""))), + ), + } + + 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("global-default-backend", + lbMethod("wrr"), + servers( + server("http://10.10.0.1:8080", weight(1)), + ), + ), + ), + frontends( + frontend("global-default-backend", + frontendName("global-default-frontend"), + passHostHeader(), + routes( + route("/", "PathPrefix:/"), + ), + ), + ), + ) + assert.Equal(t, expected, actual) +} + +func TestLoadGlobalIngressWithHttpsPortNames(t *testing.T) { + ingresses := []*extensionsv1beta1.Ingress{ + buildIngress( + iNamespace("testing"), + iSpecBackends(iSpecBackend(iIngressBackend("service1", intstr.FromString("https-global")))), + ), + } + + services := []*corev1.Service{ + buildService( + sName("service1"), + sNamespace("testing"), + sUID("1"), + sSpec( + clusterIP("10.0.0.1"), + sPorts(sPort(8443, "https-global"))), + ), + } + + endpoints := []*corev1.Endpoints{ + buildEndpoint( + eNamespace("testing"), + eName("service1"), + eUID("1"), + subset( + eAddresses(eAddress("10.10.0.1")), + ePorts(ePort(8080, ""))), + ), + } + + 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("global-default-backend", + lbMethod("wrr"), + servers( + server("https://10.10.0.1:8080", weight(1)), + ), + ), + ), + frontends( + frontend("global-default-backend", + frontendName("global-default-frontend"), + passHostHeader(), + routes( + route("/", "PathPrefix:/"), + ), + ), + ), + ) + assert.Equal(t, expected, actual) +} + func TestRuleType(t *testing.T) { tests := []struct { desc string @@ -1557,8 +1685,13 @@ func TestKubeAPIErrors(t *testing.T) { provider := Provider{} - if _, err := provider.loadIngresses(client); err != apiErr { - t.Errorf("Got error %v, wanted error %v", err, apiErr) + if _, err := provider.loadIngresses(client); err != nil { + if client.apiServiceError != nil { + assert.EqualError(t, err, "failed kube api call") + } + if client.apiEndpointsError != nil { + assert.EqualError(t, err, "failed kube api call") + } } }) }