From 0947aa901ec7b34c6a07fc0e80b114eff1dc2891 Mon Sep 17 00:00:00 2001 From: Regner Blok-Andersen Date: Thu, 9 Feb 2017 17:25:38 -0800 Subject: [PATCH 1/9] Initial support for Kubernetes ExternalName service type --- provider/kubernetes.go | 48 ++++++++++++++++++++-------------- provider/kubernetes_test.go | 51 +++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 19 deletions(-) diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 9747d60e1..0e845b42a 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -193,28 +193,38 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur if port.Port == 443 { protocol = "https" } - endpoints, exists, err := k8sClient.GetEndpoints(service.ObjectMeta.Namespace, service.ObjectMeta.Name) - if err != nil || !exists { - log.Errorf("Error retrieving endpoints %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.Name, err) - continue - } - if len(endpoints.Subsets) == 0 { - log.Warnf("Endpoints not found for %s/%s, falling back to Service ClusterIP", service.ObjectMeta.Namespace, service.ObjectMeta.Name) - templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{ - URL: protocol + "://" + service.Spec.ClusterIP + ":" + strconv.Itoa(int(port.Port)), + if service.Spec.Type == "ExternalName" { + url := protocol + "://" + service.Spec.ExternalName + name := url + + templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{ + URL: url, Weight: 1, } } else { - for _, subset := range endpoints.Subsets { - for _, address := range subset.Addresses { - url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports)) - name := url - if address.TargetRef != nil && address.TargetRef.Name != "" { - name = address.TargetRef.Name - } - templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{ - URL: url, - Weight: 1, + endpoints, exists, err := k8sClient.GetEndpoints(service.ObjectMeta.Namespace, service.ObjectMeta.Name) + if err != nil || !exists { + log.Errorf("Error retrieving endpoints %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.Name, err) + continue + } + if len(endpoints.Subsets) == 0 { + log.Warnf("Endpoints not found for %s/%s, falling back to Service ClusterIP", service.ObjectMeta.Namespace, service.ObjectMeta.Name) + templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{ + URL: protocol + "://" + service.Spec.ClusterIP + ":" + strconv.Itoa(int(port.Port)), + Weight: 1, + } + } else { + for _, subset := range endpoints.Subsets { + for _, address := range subset.Addresses { + url := protocol + "://" + address.IP + ":" + strconv.Itoa(endpointPortNumber(port, subset.Ports)) + name := url + if address.TargetRef != nil && address.TargetRef.Name != "" { + name = address.TargetRef.Name + } + templateObjects.Backends[r.Host+pa.Path].Servers[name] = types.Server{ + URL: url, + Weight: 1, + } } } } diff --git a/provider/kubernetes_test.go b/provider/kubernetes_test.go index a3866f9cc..572facd18 100644 --- a/provider/kubernetes_test.go +++ b/provider/kubernetes_test.go @@ -31,6 +31,13 @@ func TestLoadIngresses(t *testing.T) { ServicePort: intstr.FromInt(80), }, }, + { + Path: "/namedthing", + Backend: v1beta1.IngressBackend{ + ServiceName: "service4", + ServicePort: intstr.FromString("https"), + }, + }, }, }, }, @@ -110,6 +117,24 @@ func TestLoadIngresses(t *testing.T) { }, }, }, + { + ObjectMeta: v1.ObjectMeta{ + Name: "service4", + UID: "4", + Namespace: "testing", + }, + Spec: v1.ServiceSpec{ + ClusterIP: "10.0.0.4", + Type: "ExternalName", + ExternalName: "example.com", + Ports: []v1.ServicePort{ + { + Name: "https", + Port: 443, + }, + }, + }, + }, } endpoints := []*v1.Endpoints{ { @@ -221,6 +246,19 @@ func TestLoadIngresses(t *testing.T) { Method: "wrr", }, }, + "foo/namedthing": { + Servers: map[string]types.Server{ + "https://example.com": { + URL: "https://example.com", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: &types.LoadBalancer{ + Sticky: false, + Method: "wrr", + }, + }, "bar": { Servers: map[string]types.Server{ "2": { @@ -257,6 +295,19 @@ func TestLoadIngresses(t *testing.T) { }, }, }, + "foo/namedthing": { + Backend: "foo/namedthing", + PassHostHeader: true, + Priority: len("/namedthing"), + Routes: map[string]types.Route{ + "/namedthing": { + Rule: "PathPrefix:/namedthing", + }, + "foo": { + Rule: "Host:foo", + }, + }, + }, "bar": { Backend: "bar", PassHostHeader: true, From 0b1dd69b010d843de204dd9c199b36267ac5ef46 Mon Sep 17 00:00:00 2001 From: Regner Blok-Andersen Date: Fri, 10 Feb 2017 03:05:59 -0800 Subject: [PATCH 2/9] Added support for passHostHeader annotation on ingresses --- provider/kubernetes.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 0e845b42a..9efcc71ec 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -107,7 +107,6 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur map[string]*types.Backend{}, map[string]*types.Frontend{}, } - PassHostHeader := provider.getPassHostHeader() for _, i := range ingresses { for _, r := range i.Spec.Rules { if r.HTTP == nil { @@ -124,6 +123,18 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur }, } } + + PassHostHeader := provider.getPassHostHeader() + + passHostHeaderAnnotation := i.Annotations["traefik.frontend.passHostHeader"] + switch passHostHeaderAnnotation { + case "true": + PassHostHeader = true + case "false": + PassHostHeader = false + + } + if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists { templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{ Backend: r.Host + pa.Path, From 4d3aede5d3bcb86f4830dc5ba22588d55d9b07c4 Mon Sep 17 00:00:00 2001 From: Regner Blok-Andersen Date: Fri, 10 Feb 2017 03:27:30 -0800 Subject: [PATCH 3/9] Added tests for ingress passHostHeader annotation --- provider/kubernetes_test.go | 161 ++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/provider/kubernetes_test.go b/provider/kubernetes_test.go index 572facd18..0bea31f86 100644 --- a/provider/kubernetes_test.go +++ b/provider/kubernetes_test.go @@ -1575,6 +1575,167 @@ func TestServiceAnnotations(t *testing.T) { } } +func TestIngressAnnotations(t *testing.T) { + ingresses := []*v1beta1.Ingress{ + { + ObjectMeta: v1.ObjectMeta{ + Namespace: "testing", + Annotations: map[string]string{ + "traefik.frontend.passHostHeader": "false", + }, + }, + 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), + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + ObjectMeta: v1.ObjectMeta{ + Namespace: "testing", + Annotations: map[string]string{ + "traefik.frontend.passHostHeader": "true", + }, + }, + Spec: v1beta1.IngressSpec{ + Rules: []v1beta1.IngressRule{ + { + Host: "other", + IngressRuleValue: v1beta1.IngressRuleValue{ + HTTP: &v1beta1.HTTPIngressRuleValue{ + Paths: []v1beta1.HTTPIngressPath{ + { + Path: "/stuff", + 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 := Kubernetes{} + 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{ + Sticky: false, + Method: "wrr", + }, + }, + "other/stuff": { + Servers: map[string]types.Server{ + "http://example.com": { + URL: "http://example.com", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: &types.LoadBalancer{ + Sticky: false, + Method: "wrr", + }, + }, + }, + Frontends: map[string]*types.Frontend{ + "foo/bar": { + Backend: "foo/bar", + PassHostHeader: false, + Priority: len("/bar"), + Routes: map[string]types.Route{ + "/bar": { + Rule: "PathPrefix:/bar", + }, + "foo": { + Rule: "Host:foo", + }, + }, + }, + "other/stuff": { + Backend: "other/stuff", + PassHostHeader: true, + Priority: len("/stuff"), + Routes: map[string]types.Route{ + "/stuff": { + Rule: "PathPrefix:/stuff", + }, + "other": { + Rule: "Host:other", + }, + }, + }, + }, + } + + actualJSON, _ := json.Marshal(actual) + expectedJSON, _ := json.Marshal(expected) + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("expected %+v, got %+v", string(expectedJSON), string(actualJSON)) + } +} + type clientMock struct { ingresses []*v1beta1.Ingress services []*v1.Service From 931ee55e1d0a3f01f8e51a0f8cf2ee707806e293 Mon Sep 17 00:00:00 2001 From: Regner Blok-Andersen Date: Tue, 14 Feb 2017 11:52:54 -0800 Subject: [PATCH 4/9] Added default case for PassHostHeader that logs a warning. --- provider/kubernetes.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 9efcc71ec..7b3cc0176 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -132,7 +132,8 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur PassHostHeader = true case "false": PassHostHeader = false - + default: + log.Warnf("Unknown value of %s for traefik.frontend.passHostHeader, falling back to %s", passHostHeaderAnnotation, PassHostHeader) } if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists { From 96e6c9cef257020518d70eb190a861ef3a51d28a Mon Sep 17 00:00:00 2001 From: Regner Blok-Andersen Date: Tue, 14 Feb 2017 11:53:35 -0800 Subject: [PATCH 5/9] Split the if/or statement when requesting endpoints from the k8s service so that it now provides two unique log statements. --- provider/kubernetes.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 7b3cc0176..b8e37fdb2 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -215,10 +215,14 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur } } else { endpoints, exists, err := k8sClient.GetEndpoints(service.ObjectMeta.Namespace, service.ObjectMeta.Name) - if err != nil || !exists { - log.Errorf("Error retrieving endpoints %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.Name, err) + if err != nil { + log.Errorf("Error while retrieving endpoints from k8s API %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.Name, err) + continue + } else if !exists { + log.Errorf("Service not found for %s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name) continue } + if len(endpoints.Subsets) == 0 { log.Warnf("Endpoints not found for %s/%s, falling back to Service ClusterIP", service.ObjectMeta.Namespace, service.ObjectMeta.Name) templateObjects.Backends[r.Host+pa.Path].Servers[string(service.UID)] = types.Server{ From c8cf5f8c4463a063aea4792a7de8e8578a85a06d Mon Sep 17 00:00:00 2001 From: Regner Blok-Andersen Date: Tue, 14 Feb 2017 11:54:27 -0800 Subject: [PATCH 6/9] Added a test to make sure passing an invalid value to traefik.frontend.passHostHeader results in falling back correctly. --- provider/kubernetes_test.go | 107 ++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/provider/kubernetes_test.go b/provider/kubernetes_test.go index 0bea31f86..132fdf90e 100644 --- a/provider/kubernetes_test.go +++ b/provider/kubernetes_test.go @@ -1736,6 +1736,113 @@ func TestIngressAnnotations(t *testing.T) { } } +func TestInvalidPassHostHeaderValue(t *testing.T) { + ingresses := []*v1beta1.Ingress{ + { + ObjectMeta: v1.ObjectMeta{ + Namespace: "testing", + Annotations: map[string]string{ + "traefik.frontend.passHostHeader": "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 := Kubernetes{} + 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{ + Sticky: false, + Method: "wrr", + }, + }, + }, + Frontends: map[string]*types.Frontend{ + "foo/bar": { + Backend: "foo/bar", + PassHostHeader: true, + Priority: len("/bar"), + Routes: map[string]types.Route{ + "/bar": { + Rule: "PathPrefix:/bar", + }, + "foo": { + Rule: "Host:foo", + }, + }, + }, + }, + } + + actualJSON, _ := json.Marshal(actual) + expectedJSON, _ := json.Marshal(expected) + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("expected %+v, got %+v", string(expectedJSON), string(actualJSON)) + } +} + type clientMock struct { ingresses []*v1beta1.Ingress services []*v1.Service From 1e10fc2e306ae74c5a4894785e6e0afe57fe86e3 Mon Sep 17 00:00:00 2001 From: Regner Blok-Andersen Date: Tue, 14 Feb 2017 14:57:09 -0800 Subject: [PATCH 7/9] Simplifying else if statement to be cleaner --- provider/kubernetes.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/provider/kubernetes.go b/provider/kubernetes.go index b8e37fdb2..6fe6ca848 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -218,7 +218,9 @@ func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configur if err != nil { log.Errorf("Error while retrieving endpoints from k8s API %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.Name, err) continue - } else if !exists { + } + + if !exists { log.Errorf("Service not found for %s/%s", service.ObjectMeta.Namespace, service.ObjectMeta.Name) continue } From 66cc9a075c7b013de6112f7f1726004f2fe6d9d2 Mon Sep 17 00:00:00 2001 From: Regner Blok-Andersen Date: Wed, 15 Feb 2017 13:37:47 -0800 Subject: [PATCH 8/9] First pass of documentation for passHostHeader kubernetes annotation --- docs/user-guide/kubernetes.md | 45 +++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index 643bd2f76..05225c6cd 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -428,3 +428,48 @@ You should now be able to visit the websites in your browser. * [cheeses.local/stilton](http://cheeses.local/stilton/) * [cheeses.local/cheddar](http://cheeses.local/cheddar/) * [cheeses.local/wensleydale](http://cheeses.local/wensleydale/) + +## Disable passing the Host header +By default Træfɪk will pass the incoming Host header on to the upstream resource. There +are times however where you may not want this to be the case. For example if your service +is of the ExternalName type. + +To disable passing the Host header set the "traefik.frontend.passHostHeader" annotation on +your ingress to "false". + +Here is an example ingress definition: +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: example + annotations: + traefik.frontend.passHostHeader: "false" +spec: + rules: + - host: example.com + http: + paths: + - path: /static + backend: + serviceName: static + servicePort: https +``` + +And an example service definition: +```yaml +apiVersion: v1 +kind: Service +metadata: + name: static +spec: + ports: + - name: https + port: 443 + type: ExternalName + externalName: static.otherdomain.com +``` + +If you were to visit example.com/static the request would then be passed onto +static.otherdomain.com/static and static.otherdomain.com would receive the +request with the Host header being static.otherdomain.com. \ No newline at end of file From 49466d0d1452dcb6ab4154b23b9321552ced552c Mon Sep 17 00:00:00 2001 From: Regner Blok-Andersen Date: Wed, 15 Feb 2017 16:11:31 -0800 Subject: [PATCH 9/9] Added documentation about defining the passing of host header globaly --- docs/user-guide/kubernetes.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index 05225c6cd..44eb68a96 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -434,8 +434,15 @@ By default Træfɪk will pass the incoming Host header on to the upstream resour are times however where you may not want this to be the case. For example if your service is of the ExternalName type. -To disable passing the Host header set the "traefik.frontend.passHostHeader" annotation on -your ingress to "false". +### Disable entirely +Add the following to your toml config: +```toml +disablePassHostHeaders = true +``` + +### Disable per ingress +To disable passing the Host header per ingress resource set the "traefik.frontend.passHostHeader" +annotation on your ingress to "false". Here is an example ingress definition: ```yaml @@ -472,4 +479,8 @@ spec: If you were to visit example.com/static the request would then be passed onto static.otherdomain.com/static and static.otherdomain.com would receive the -request with the Host header being static.otherdomain.com. \ No newline at end of file +request with the Host header being static.otherdomain.com. + +Note: The per ingress annotation overides whatever the global value is set to. So you +could set `disablePassHostHeaders` to true in your toml file and then enable passing +the host header per ingress if you wanted. \ No newline at end of file