From 7ed4ae2f8c5d40e5aa1888009d25cdae01154a33 Mon Sep 17 00:00:00 2001 From: ryarnyah Date: Mon, 20 Nov 2017 02:12:03 +0100 Subject: [PATCH] Add labels for `traefik.frontend.entryPoints` & `PassTLSCert` to Kubernetes --- docs/configuration/backends/kubernetes.md | 11 + provider/kubernetes/kubernetes.go | 59 ++++- provider/kubernetes/kubernetes_test.go | 295 ++++++++++++++++++++++ types/common_label.go | 1 + 4 files changed, 352 insertions(+), 14 deletions(-) diff --git a/docs/configuration/backends/kubernetes.md b/docs/configuration/backends/kubernetes.md index 41971496d..59fc74707 100644 --- a/docs/configuration/backends/kubernetes.md +++ b/docs/configuration/backends/kubernetes.md @@ -57,6 +57,13 @@ See also [Kubernetes user guide](/user-guide/kubernetes). # Default: false # # disablePassHostHeaders = true + +# Enable PassTLSCert Headers. +# +# Optional +# Default: false +# +# enablePassTLSCert = true ``` ### `endpoint` @@ -90,6 +97,10 @@ Annotations can be used on containers to override default behaviour for the whol Override the default frontend rule priority. - `traefik.frontend.redirect: https`: Enables Redirect to another entryPoint for that frontend (e.g. HTTPS). +- `traefik.frontend.entryPoints: http,https` + Override the default frontend endpoints. +- `traefik.frontend.passTLSCert: true` + Override the default frontend PassTLSCert value. Default: `false`. Annotations can be used on the Kubernetes service to override default behaviour: diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index d22795741..a7fd6ecb6 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -47,6 +47,7 @@ type Provider struct { Token string `description:"Kubernetes bearer token (not needed for in-cluster client)"` CertAuthFilePath string `description:"Kubernetes certificate authority file path (not needed for in-cluster client)"` DisablePassHostHeaders bool `description:"Kubernetes disable PassHost Headers" export:"true"` + 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"` lastConfiguration safe.Safe @@ -165,25 +166,16 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) } } - PassHostHeader := p.getPassHostHeader() - - passHostHeaderAnnotation, ok := i.Annotations[types.LabelFrontendPassHostHeader] - switch { - case !ok: - // No op. - case passHostHeaderAnnotation == "false": - PassHostHeader = false - case passHostHeaderAnnotation == "true": - PassHostHeader = true - default: - log.Warnf("Unknown value '%s' for %s, falling back to %s", passHostHeaderAnnotation, types.LabelFrontendPassHostHeader, PassHostHeader) - } + passHostHeader := getAnnotationPassHostHeader(i, p) + passTLSCert := getAnnotationPassTLSCert(i, p) if realm := i.Annotations[annotationKubernetesAuthRealm]; realm != "" && realm != traefikDefaultRealm { log.Errorf("Value for annotation %q on ingress %s/%s invalid: no realm customization supported", annotationKubernetesAuthRealm, i.ObjectMeta.Namespace, i.ObjectMeta.Name) delete(templateObjects.Backends, r.Host+pa.Path) continue } + entryPoints := getEntrypoints(i) + whitelistSourceRangeAnnotation := i.Annotations[annotationKubernetesWhitelistSourceRange] whitelistSourceRange := provider.SplitAndTrimString(whitelistSourceRangeAnnotation) @@ -200,12 +192,14 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{ Backend: r.Host + pa.Path, - PassHostHeader: PassHostHeader, + PassHostHeader: passHostHeader, + PassTLSCert: passTLSCert, Routes: make(map[string]types.Route), Priority: priority, BasicAuth: basicAuthCreds, WhitelistSourceRange: whitelistSourceRange, Redirect: entryPointRedirect, + EntryPoints: entryPoints, } } if len(r.Host) > 0 { @@ -318,6 +312,39 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) return &templateObjects, nil } +func getEntrypoints(i *v1beta1.Ingress) []string { + entrypointsAnnotation, ok := i.Annotations[types.LabelFrontendEntryPoints] + if ok { + return strings.Split(entrypointsAnnotation, ",") + } + return nil + +} + +func getBoolAnnotation(meta v1.ObjectMeta, name string, defaultValue bool) bool { + annotationValue := defaultValue + annotationStringValue, ok := meta.Annotations[name] + switch { + case !ok: + // No op. + case annotationStringValue == "false": + annotationValue = false + case annotationStringValue == "true": + annotationValue = true + default: + log.Warnf("Unknown value '%s' for %s, falling back to %s", name, types.LabelFrontendPassTLSCert, defaultValue) + } + return annotationValue +} + +func getAnnotationPassHostHeader(i *v1beta1.Ingress, p *Provider) bool { + return getBoolAnnotation(i.ObjectMeta, types.LabelFrontendPassHostHeader, p.getPassHostHeader()) +} + +func getAnnotationPassTLSCert(i *v1beta1.Ingress, p *Provider) bool { + return getBoolAnnotation(i.ObjectMeta, types.LabelFrontendPassTLSCert, p.getPassTLSCert()) +} + func getRuleForPath(pa v1beta1.HTTPIngressPath, i *v1beta1.Ingress) string { if len(pa.Path) == 0 { return "" @@ -442,6 +469,10 @@ func (p *Provider) getPassHostHeader() bool { return !p.DisablePassHostHeaders } +func (p *Provider) getPassTLSCert() bool { + return p.EnablePassTLSCert +} + func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Configuration { var FuncMap = template.FuncMap{} configuration, err := p.GetConfiguration("templates/kubernetes.tmpl", FuncMap, templateObjects) diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index c3107c86a..767dbfe1f 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -508,6 +508,92 @@ func TestGetPassHostHeader(t *testing.T) { assert.Equal(t, expected, actual) } +func TestGetPassTLSCert(t *testing.T) { + ingresses := []*v1beta1.Ingress{{ + ObjectMeta: v1.ObjectMeta{ + Namespace: "awesome", + }, + Spec: v1beta1.IngressSpec{ + Rules: []v1beta1.IngressRule{ + { + Host: "foo", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/bar", + Backend: v1beta1.IngressBackend{ + ServiceName: "service1", + ServicePort: intstr.FromInt(801), + }, + }, + }, + }, + }, + }, + }, + }, + }} + services := []*v1.Service{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "service1", + Namespace: "awesome", + UID: "1", + }, + Spec: v1.ServiceSpec{ + ClusterIP: "10.0.0.1", + Ports: []v1.ServicePort{ + { + Name: "http", + Port: 801, + }, + }, + }, + }, + } + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + watchChan: watchChan, + } + provider := Provider{EnablePassTLSCert: true} + actual, err := provider.loadIngresses(client) + if err != nil { + t.Fatalf("error %+v", err) + } + + expected := &types.Configuration{ + Backends: map[string]*types.Backend{ + "foo/bar": { + Servers: map[string]types.Server{}, + CircuitBreaker: nil, + LoadBalancer: &types.LoadBalancer{ + Method: "wrr", + }, + }, + }, + Frontends: map[string]*types.Frontend{ + "foo/bar": { + Backend: "foo/bar", + PassTLSCert: true, + PassHostHeader: true, + Routes: map[string]types.Route{ + "/bar": { + Rule: "PathPrefix:/bar", + }, + "foo": { + Rule: "Host:foo", + }, + }, + }, + }, + } + + assert.Equal(t, expected, actual) +} + func TestOnlyReferencesServicesFromOwnNamespace(t *testing.T) { ingresses := []*v1beta1.Ingress{ { @@ -976,6 +1062,64 @@ func TestIngressAnnotations(t *testing.T) { }, }, }, + { + ObjectMeta: v1.ObjectMeta{ + Namespace: "testing", + Annotations: map[string]string{ + "kubernetes.io/ingress.class": "traefik", + types.LabelFrontendPassTLSCert: "true", + }, + }, + Spec: v1beta1.IngressSpec{ + Rules: []v1beta1.IngressRule{ + { + Host: "other", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/sslstuff", + Backend: v1beta1.IngressBackend{ + ServiceName: "service1", + ServicePort: intstr.FromInt(80), + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + ObjectMeta: v1.ObjectMeta{ + Namespace: "testing", + Annotations: map[string]string{ + "kubernetes.io/ingress.class": "traefik", + types.LabelFrontendEntryPoints: "http,https", + }, + }, + Spec: v1beta1.IngressSpec{ + Rules: []v1beta1.IngressRule{ + { + Host: "other", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/", + Backend: v1beta1.IngressBackend{ + ServiceName: "service1", + ServicePort: intstr.FromInt(80), + }, + }, + }, + }, + }, + }, + }, + }, + }, { ObjectMeta: v1.ObjectMeta{ Namespace: "testing", @@ -1222,6 +1366,30 @@ func TestIngressAnnotations(t *testing.T) { Method: "wrr", }, }, + "other/": { + Servers: map[string]types.Server{ + "http://example.com": { + URL: "http://example.com", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: &types.LoadBalancer{ + Method: "wrr", + }, + }, + "other/sslstuff": { + Servers: map[string]types.Server{ + "http://example.com": { + URL: "http://example.com", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: &types.LoadBalancer{ + Method: "wrr", + }, + }, "basic/auth": { Servers: map[string]types.Server{ "http://example.com": { @@ -1299,6 +1467,32 @@ func TestIngressAnnotations(t *testing.T) { }, Redirect: "", }, + "other/": { + Backend: "other/", + PassHostHeader: true, + EntryPoints: []string{"http", "https"}, + Routes: map[string]types.Route{ + "/": { + Rule: "PathPrefix:/", + }, + "other": { + Rule: "Host:other", + }, + }, + }, + "other/sslstuff": { + Backend: "other/sslstuff", + PassHostHeader: true, + PassTLSCert: true, + Routes: map[string]types.Route{ + "/sslstuff": { + Rule: "PathPrefix:/sslstuff", + }, + "other": { + Rule: "Host:other", + }, + }, + }, "basic/auth": { Backend: "basic/auth", PassHostHeader: true, @@ -1464,6 +1658,107 @@ func TestPriorityHeaderValue(t *testing.T) { assert.Equal(t, expected, actual) } +func TestInvalidPassTLSCertValue(t *testing.T) { + ingresses := []*v1beta1.Ingress{ + { + ObjectMeta: v1.ObjectMeta{ + Namespace: "testing", + Annotations: map[string]string{ + types.LabelFrontendPassTLSCert: "herpderp", + }, + }, + Spec: v1beta1.IngressSpec{ + Rules: []v1beta1.IngressRule{ + { + Host: "foo", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/bar", + Backend: v1beta1.IngressBackend{ + ServiceName: "service1", + ServicePort: intstr.FromInt(80), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + services := []*v1.Service{ + { + ObjectMeta: v1.ObjectMeta{ + Name: "service1", + UID: "1", + Namespace: "testing", + }, + Spec: v1.ServiceSpec{ + ClusterIP: "10.0.0.1", + Type: "ExternalName", + ExternalName: "example.com", + Ports: []v1.ServicePort{ + { + Name: "http", + Port: 80, + }, + }, + }, + }, + } + + endpoints := []*v1.Endpoints{} + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + endpoints: endpoints, + watchChan: watchChan, + } + provider := Provider{} + actual, err := provider.loadIngresses(client) + if err != nil { + t.Fatalf("error %+v", err) + } + + expected := &types.Configuration{ + Backends: map[string]*types.Backend{ + "foo/bar": { + Servers: map[string]types.Server{ + "http://example.com": { + URL: "http://example.com", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: &types.LoadBalancer{ + Method: "wrr", + }, + }, + }, + Frontends: map[string]*types.Frontend{ + "foo/bar": { + Backend: "foo/bar", + PassTLSCert: false, + PassHostHeader: true, + Routes: map[string]types.Route{ + "/bar": { + Rule: "PathPrefix:/bar", + }, + "foo": { + Rule: "Host:foo", + }, + }, + }, + }, + } + + assert.Equal(t, expected, actual) +} + func TestInvalidPassHostHeaderValue(t *testing.T) { ingresses := []*v1beta1.Ingress{ { diff --git a/types/common_label.go b/types/common_label.go index 794420c73..ed301c6be 100644 --- a/types/common_label.go +++ b/types/common_label.go @@ -17,6 +17,7 @@ const ( LabelFrontendRequestHeader = LabelPrefix + "frontend.headers.customrequestheaders" LabelFrontendResponseHeader = LabelPrefix + "frontend.headers.customresponseheaders" LabelFrontendPassHostHeader = LabelPrefix + "frontend.passHostHeader" + LabelFrontendPassTLSCert = LabelPrefix + "frontend.passTLSCert" LabelFrontendPriority = LabelPrefix + "frontend.priority" LabelFrontendRule = LabelPrefix + "frontend.rule" LabelFrontendRuleType = LabelPrefix + "frontend.rule.type"