From 04ebd9d46acf3fa1db0466c46f4caefcf3c8e3e9 Mon Sep 17 00:00:00 2001 From: Yuvi Panda Date: Thu, 1 Feb 2018 10:04:04 -0800 Subject: [PATCH] Allow custom value for kubernetes.io/ingress.class annotation --- cmd/traefik/configuration.go | 2 - docs/configuration/backends/kubernetes.md | 11 ++ provider/kubernetes/kubernetes.go | 24 ++- provider/kubernetes/kubernetes_test.go | 173 ++++++++++++++++++++-- 4 files changed, 188 insertions(+), 22 deletions(-) diff --git a/cmd/traefik/configuration.go b/cmd/traefik/configuration.go index fd9da28b6..2b04e20fb 100644 --- a/cmd/traefik/configuration.go +++ b/cmd/traefik/configuration.go @@ -130,8 +130,6 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { //default Kubernetes var defaultKubernetes kubernetes.Provider defaultKubernetes.Watch = true - defaultKubernetes.Endpoint = "" - defaultKubernetes.LabelSelector = "" defaultKubernetes.Constraints = types.Constraints{} // default Mesos diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 49d3eb510..86aec53bf 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -50,6 +50,17 @@ See also [Kubernetes user guide](/user-guide/kubernetes). # # labelselector = "A and not B" +# Value of `kubernetes.io/ingress.class` annotation that identifies Ingress objects to be processed. +# If the parameter is non-empty, only Ingresses containing an annotation with the same value are processed. +# Otherwise, Ingresses missing the annotation, having an empty value, or the value `traefik` are processed. +# +# Note : `ingressClass` option must begin with the "traefik" prefix. +# +# Optional +# Default: empty +# +# ingressClass = "traefik-internal" + # Disable PassHost Headers. # # Optional diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 078bc47fa..47298e901 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -30,10 +30,10 @@ import ( var _ provider.Provider = (*Provider)(nil) const ( - ruleTypePathPrefix = "PathPrefix" - ruleTypeReplacePath = "ReplacePath" - - traefikDefaultRealm = "traefik" + ruleTypePathPrefix = "PathPrefix" + ruleTypeReplacePath = "ReplacePath" + traefikDefaultRealm = "traefik" + traefikDefaultIngressClass = "traefik" ) // Provider holds configurations of the provider. @@ -46,6 +46,7 @@ type Provider struct { EnablePassTLSCert bool `description:"Kubernetes enable Pass TLS Client Certs" export:"true"` Namespaces Namespaces `description:"Kubernetes namespaces" export:"true"` LabelSelector string `description:"Kubernetes api label selector to use" export:"true"` + IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for" export:"true"` lastConfiguration safe.Safe } @@ -76,6 +77,12 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s return err } + // We require that IngressClasses start with `traefik` to reduce chances of + // conflict with other Ingress Providers + if len(p.IngressClass) > 0 && !strings.HasPrefix(p.IngressClass, traefikDefaultIngressClass) { + return fmt.Errorf("value for IngressClass has to be empty or start with the prefix %q, instead found %q", traefikDefaultIngressClass, p.IngressClass) + } + k8sClient, err := p.newK8sClient() if err != nil { return err @@ -147,7 +154,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) annotationIngressClass := getAnnotationName(i.Annotations, annotationKubernetesIngressClass) ingressClass := i.Annotations[annotationIngressClass] - if !shouldProcessIngress(ingressClass) { + if !p.shouldProcessIngress(ingressClass) { continue } @@ -451,8 +458,11 @@ func equalPorts(servicePort v1.ServicePort, ingressPort intstr.IntOrString) bool return false } -func shouldProcessIngress(ingressClass string) bool { - return ingressClass == "" || ingressClass == "traefik" +func (p *Provider) shouldProcessIngress(annotationIngressClass string) bool { + if len(p.IngressClass) == 0 { + return len(annotationIngressClass) == 0 || annotationIngressClass == traefikDefaultIngressClass + } + return annotationIngressClass == p.IngressClass } func getFrontendRedirect(i *v1beta1.Ingress) *types.Redirect { diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index 9c074d87f..6f8e345b6 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -616,7 +616,7 @@ func TestIngressAnnotations(t *testing.T) { buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesPreserveHost, "true"), - iAnnotation(annotationKubernetesIngressClass, "traefik"), + iAnnotation(annotationKubernetesIngressClass, traefikDefaultRealm), iRules( iRule( iHost("other"), @@ -626,7 +626,7 @@ func TestIngressAnnotations(t *testing.T) { buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesPassTLSCert, "true"), - iAnnotation(annotationKubernetesIngressClass, "traefik"), + iAnnotation(annotationKubernetesIngressClass, traefikDefaultRealm), iRules( iRule( iHost("other"), @@ -636,7 +636,7 @@ func TestIngressAnnotations(t *testing.T) { buildIngress( iNamespace("testing"), iAnnotation(annotationKubernetesFrontendEntryPoints, "http,https"), - iAnnotation(annotationKubernetesIngressClass, "traefik"), + iAnnotation(annotationKubernetesIngressClass, traefikDefaultRealm), iRules( iRule( iHost("other"), @@ -655,7 +655,7 @@ func TestIngressAnnotations(t *testing.T) { ), buildIngress( iNamespace("testing"), - iAnnotation(annotationKubernetesIngressClass, "somethingOtherThanTraefik"), + iAnnotation(annotationKubernetesIngressClass, traefikDefaultRealm+"-other"), iRules( iRule( iHost("herp"), @@ -664,7 +664,6 @@ func TestIngressAnnotations(t *testing.T) { ), buildIngress( iNamespace("testing"), - iAnnotation(annotationKubernetesIngressClass, "traefik"), iAnnotation(annotationKubernetesWhitelistSourceRange, "1.1.1.1/24, 1234:abcd::42/32"), iRules( iRule( @@ -692,7 +691,6 @@ func TestIngressAnnotations(t *testing.T) { ), buildIngress( iNamespace("testing"), - iAnnotation(annotationKubernetesIngressClass, "traefik"), iAnnotation(annotationKubernetesRedirectEntryPoint, "https"), iRules( iRule( @@ -790,14 +788,16 @@ rateset: ), } - secrets := []*v1.Secret{{ - ObjectMeta: v1.ObjectMeta{ - Name: "mySecret", - UID: "1", - Namespace: "testing", + secrets := []*v1.Secret{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "mySecret", + UID: "1", + Namespace: "testing", + }, + Data: map[string][]byte{"auth": []byte("myUser:myEncodedPW")}, }, - Data: map[string][]byte{"auth": []byte("myUser:myEncodedPW")}, - }} + } watchChan := make(chan interface{}) client := clientMock{ @@ -999,6 +999,153 @@ rateset: assert.Equal(t, expected, actual) } +func TestIngressClassAnnotation(t *testing.T) { + ingresses := []*v1beta1.Ingress{ + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesIngressClass, traefikDefaultIngressClass), + iRules( + iRule( + iHost("other"), + iPaths(onePath(iPath("/stuff"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesIngressClass, ""), + iRules( + iRule( + iHost("other"), + iPaths(onePath(iPath("/sslstuff"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + buildIngress( + iNamespace("testing"), + iRules( + iRule( + iHost("other"), + iPaths(onePath(iPath("/"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + buildIngress( + iNamespace("testing"), + iAnnotation(annotationKubernetesIngressClass, traefikDefaultIngressClass+"-other"), + iRules( + iRule( + iHost("herp"), + iPaths(onePath(iPath("/derp"), iBackend("service1", intstr.FromInt(80))))), + ), + ), + } + + services := []*v1.Service{ + buildService( + sName("service1"), + sNamespace("testing"), + sUID("1"), + sSpec( + clusterIP("10.0.0.1"), + sType("ExternalName"), + sExternalName("example.com"), + sPorts(sPort(80, "http"))), + ), + } + + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + watchChan: watchChan, + } + + testCases := []struct { + desc string + provider Provider + expected *types.Configuration + }{ + { + desc: "Empty IngressClass annotation", + provider: Provider{}, + expected: buildConfiguration( + backends( + backend("other/stuff", + servers( + server("http://example.com", weight(1)), + server("http://example.com", weight(1))), + lbMethod("wrr"), + ), + backend("other/", + servers( + server("http://example.com", weight(1)), + server("http://example.com", weight(1))), + lbMethod("wrr"), + ), + backend("other/sslstuff", + servers( + server("http://example.com", weight(1)), + server("http://example.com", weight(1))), + lbMethod("wrr"), + ), + ), + frontends( + frontend("other/stuff", + passHostHeader(), + routes( + route("/stuff", "PathPrefix:/stuff"), + route("other", "Host:other")), + ), + frontend("other/", + passHostHeader(), + routes( + route("/", "PathPrefix:/"), + route("other", "Host:other")), + ), + frontend("other/sslstuff", + passHostHeader(), + routes( + route("/sslstuff", "PathPrefix:/sslstuff"), + route("other", "Host:other")), + ), + ), + ), + }, + { + desc: "Provided IngressClass annotation", + provider: Provider{IngressClass: traefikDefaultRealm + "-other"}, + expected: buildConfiguration( + backends( + backend("herp/derp", + servers( + server("http://example.com", weight(1)), + server("http://example.com", weight(1))), + lbMethod("wrr"), + ), + ), + frontends( + frontend("herp/derp", + passHostHeader(), + routes( + route("/derp", "PathPrefix:/derp"), + route("herp", "Host:herp")), + ), + ), + ), + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual, err := test.provider.loadIngresses(client) + require.NoError(t, err, "error loading ingresses") + + assert.Equal(t, test.expected, actual) + }) + } +} + func TestPriorityHeaderValue(t *testing.T) { ingresses := []*v1beta1.Ingress{ buildIngress(