diff --git a/docs/user-guide/kubernetes.md b/docs/user-guide/kubernetes.md index 643bd2f76..44eb68a96 100644 --- a/docs/user-guide/kubernetes.md +++ b/docs/user-guide/kubernetes.md @@ -428,3 +428,59 @@ 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. + +### 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 +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. + +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 diff --git a/provider/kubernetes.go b/provider/kubernetes.go index 9747d60e1..6fe6ca848 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,19 @@ 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 + 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 { templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{ Backend: r.Host + pa.Path, @@ -193,28 +205,44 @@ 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 { + log.Errorf("Error while retrieving endpoints from k8s API %s/%s: %v", service.ObjectMeta.Namespace, service.ObjectMeta.Name, err) + continue + } + + 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{ + 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..132fdf90e 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, @@ -1524,6 +1575,274 @@ 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)) + } +} + +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