From 301a463aebc90cebbccc202f0d5fe7170c6ee1f5 Mon Sep 17 00:00:00 2001 From: Ed Robinson Date: Thu, 28 Apr 2016 01:23:55 +0100 Subject: [PATCH] Adds option to namespace k8s ingresses If the flag kubernetes.namespaces is set... Then we only select ingresses from that/those namespace(s) This allows multiple instances of traefik to independently load balance for each namespace. This could be for logical or security reasons. Addresses #336 --- cmd.go | 1 + docs/toml.md | 1 + provider/k8s/client.go | 2 +- provider/kubernetes.go | 11 +- provider/kubernetes_test.go | 436 +++++++++++++++++++++++++++++++++++- traefik.sample.toml | 1 + 6 files changed, 449 insertions(+), 3 deletions(-) diff --git a/cmd.go b/cmd.go index 13796957b..f4240c4f4 100644 --- a/cmd.go +++ b/cmd.go @@ -172,6 +172,7 @@ func init() { traefikCmd.PersistentFlags().BoolVar(&arguments.kubernetes, "kubernetes", false, "Enable Kubernetes backend") traefikCmd.PersistentFlags().StringVar(&arguments.Kubernetes.Endpoint, "kubernetes.endpoint", "127.0.0.1:8080", "Kubernetes server endpoint") + traefikCmd.PersistentFlags().StringSliceVar(&arguments.Kubernetes.Namespaces, "kubernetes.namespaces", []string{}, "Kubernetes namespaces") _ = viper.BindPFlag("configFile", traefikCmd.PersistentFlags().Lookup("configFile")) _ = viper.BindPFlag("graceTimeOut", traefikCmd.PersistentFlags().Lookup("graceTimeOut")) diff --git a/docs/toml.md b/docs/toml.md index 6b349e983..3d1845a9c 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -648,6 +648,7 @@ Træfɪk can be configured to use Kubernetes Ingress as a backend configuration: # Optional # # endpoint = "http://localhost:8080" +# namespaces = ["default","production"] ``` You can find here an example [ingress](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s.ingress.yaml) and [replication controller](https://raw.githubusercontent.com/containous/traefik/master/examples/k8s.rc.yaml). diff --git a/provider/k8s/client.go b/provider/k8s/client.go index b03bb0e7d..3122c9f44 100644 --- a/provider/k8s/client.go +++ b/provider/k8s/client.go @@ -49,7 +49,7 @@ func NewClient(baseURL string, caCert []byte, token string) (Client, error) { }, nil } -// GetIngresses returns all services in the cluster +// GetIngresses returns all ingresses in the cluster func (c *clientImpl) GetIngresses(predicate func(Ingress) bool) ([]Ingress, error) { getURL := c.endpointURL + extentionsEndpoint + defaultIngress diff --git a/provider/kubernetes.go b/provider/kubernetes.go index dc7b395ff..7f0758282 100644 --- a/provider/kubernetes.go +++ b/provider/kubernetes.go @@ -23,6 +23,7 @@ const ( type Kubernetes struct { BaseProvider `mapstructure:",squash"` Endpoint string + Namespaces []string } func (provider *Kubernetes) createClient() (k8s.Client, error) { @@ -123,7 +124,15 @@ func (provider *Kubernetes) Provide(configurationChan chan<- types.ConfigMessage func (provider *Kubernetes) loadIngresses(k8sClient k8s.Client) (*types.Configuration, error) { ingresses, err := k8sClient.GetIngresses(func(ingress k8s.Ingress) bool { - return true + if len(provider.Namespaces) == 0 { + return true + } + for _, n := range provider.Namespaces { + if ingress.ObjectMeta.Namespace == n { + return true + } + } + return false }) if err != nil { log.Errorf("Error retrieving ingresses: %+v", err) diff --git a/provider/kubernetes_test.go b/provider/kubernetes_test.go index 13daf6e58..0d110f2e1 100644 --- a/provider/kubernetes_test.go +++ b/provider/kubernetes_test.go @@ -167,6 +167,434 @@ func TestLoadIngresses(t *testing.T) { } } +func TestLoadNamespacedIngresses(t *testing.T) { + ingresses := []k8s.Ingress{ + { + ObjectMeta: k8s.ObjectMeta{ + Namespace: "awesome", + }, + Spec: k8s.IngressSpec{ + Rules: []k8s.IngressRule{ + { + Host: "foo", + IngressRuleValue: k8s.IngressRuleValue{ + HTTP: &k8s.HTTPIngressRuleValue{ + Paths: []k8s.HTTPIngressPath{ + { + Path: "/bar", + Backend: k8s.IngressBackend{ + ServiceName: "service1", + ServicePort: k8s.FromInt(801), + }, + }, + }, + }, + }, + }, + { + Host: "bar", + IngressRuleValue: k8s.IngressRuleValue{ + HTTP: &k8s.HTTPIngressRuleValue{ + Paths: []k8s.HTTPIngressPath{ + { + Backend: k8s.IngressBackend{ + ServiceName: "service3", + ServicePort: k8s.FromInt(443), + }, + }, + { + Backend: k8s.IngressBackend{ + ServiceName: "service2", + ServicePort: k8s.FromInt(802), + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + ObjectMeta: k8s.ObjectMeta{ + Namespace: "not-awesome", + }, + Spec: k8s.IngressSpec{ + Rules: []k8s.IngressRule{ + { + Host: "baz", + IngressRuleValue: k8s.IngressRuleValue{ + HTTP: &k8s.HTTPIngressRuleValue{ + Paths: []k8s.HTTPIngressPath{ + { + Path: "/baz", + Backend: k8s.IngressBackend{ + ServiceName: "service1", + ServicePort: k8s.FromInt(801), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + services := []k8s.Service{ + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service1", + UID: "1", + }, + Spec: k8s.ServiceSpec{ + ClusterIP: "10.0.0.1", + Ports: []k8s.ServicePort{ + { + Name: "http", + Port: 801, + }, + }, + }, + }, + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service2", + UID: "2", + }, + Spec: k8s.ServiceSpec{ + ClusterIP: "10.0.0.2", + Ports: []k8s.ServicePort{ + { + Port: 802, + }, + }, + }, + }, + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service3", + UID: "3", + }, + Spec: k8s.ServiceSpec{ + ClusterIP: "10.0.0.3", + Ports: []k8s.ServicePort{ + { + Name: "http", + Port: 443, + }, + }, + }, + }, + } + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + watchChan: watchChan, + } + provider := Kubernetes{ + Namespaces: []string{"awesome"}, + } + 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{ + "1": { + URL: "http://10.0.0.1:801", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + "bar": { + Servers: map[string]types.Server{ + "2": { + URL: "http://10.0.0.2:802", + Weight: 1, + }, + "3": { + URL: "https://10.0.0.3:443", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + }, + Frontends: map[string]*types.Frontend{ + "foo/bar": { + Backend: "foo/bar", + Routes: map[string]types.Route{ + "/bar": { + Rule: "PathPrefixStrip:/bar", + }, + "foo": { + Rule: "Host:foo", + }, + }, + }, + "bar": { + Backend: "bar", + Routes: map[string]types.Route{ + "bar": { + Rule: "Host:bar", + }, + }, + }, + }, + } + actualJSON, _ := json.Marshal(actual) + expectedJSON, _ := json.Marshal(expected) + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("expected %+v, got %+v", string(expectedJSON), string(actualJSON)) + } +} + +func TestLoadMultipleNamespacedIngresses(t *testing.T) { + ingresses := []k8s.Ingress{ + { + ObjectMeta: k8s.ObjectMeta{ + Namespace: "awesome", + }, + Spec: k8s.IngressSpec{ + Rules: []k8s.IngressRule{ + { + Host: "foo", + IngressRuleValue: k8s.IngressRuleValue{ + HTTP: &k8s.HTTPIngressRuleValue{ + Paths: []k8s.HTTPIngressPath{ + { + Path: "/bar", + Backend: k8s.IngressBackend{ + ServiceName: "service1", + ServicePort: k8s.FromInt(801), + }, + }, + }, + }, + }, + }, + { + Host: "bar", + IngressRuleValue: k8s.IngressRuleValue{ + HTTP: &k8s.HTTPIngressRuleValue{ + Paths: []k8s.HTTPIngressPath{ + { + Backend: k8s.IngressBackend{ + ServiceName: "service3", + ServicePort: k8s.FromInt(443), + }, + }, + { + Backend: k8s.IngressBackend{ + ServiceName: "service2", + ServicePort: k8s.FromInt(802), + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + ObjectMeta: k8s.ObjectMeta{ + Namespace: "somewhat-awesome", + }, + Spec: k8s.IngressSpec{ + Rules: []k8s.IngressRule{ + { + Host: "awesome", + IngressRuleValue: k8s.IngressRuleValue{ + HTTP: &k8s.HTTPIngressRuleValue{ + Paths: []k8s.HTTPIngressPath{ + { + Path: "/quix", + Backend: k8s.IngressBackend{ + ServiceName: "service1", + ServicePort: k8s.FromInt(801), + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + ObjectMeta: k8s.ObjectMeta{ + Namespace: "not-awesome", + }, + Spec: k8s.IngressSpec{ + Rules: []k8s.IngressRule{ + { + Host: "baz", + IngressRuleValue: k8s.IngressRuleValue{ + HTTP: &k8s.HTTPIngressRuleValue{ + Paths: []k8s.HTTPIngressPath{ + { + Path: "/baz", + Backend: k8s.IngressBackend{ + ServiceName: "service1", + ServicePort: k8s.FromInt(801), + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + services := []k8s.Service{ + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service1", + UID: "1", + }, + Spec: k8s.ServiceSpec{ + ClusterIP: "10.0.0.1", + Ports: []k8s.ServicePort{ + { + Name: "http", + Port: 801, + }, + }, + }, + }, + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service2", + UID: "2", + }, + Spec: k8s.ServiceSpec{ + ClusterIP: "10.0.0.2", + Ports: []k8s.ServicePort{ + { + Port: 802, + }, + }, + }, + }, + { + ObjectMeta: k8s.ObjectMeta{ + Name: "service3", + UID: "3", + }, + Spec: k8s.ServiceSpec{ + ClusterIP: "10.0.0.3", + Ports: []k8s.ServicePort{ + { + Name: "http", + Port: 443, + }, + }, + }, + }, + } + watchChan := make(chan interface{}) + client := clientMock{ + ingresses: ingresses, + services: services, + watchChan: watchChan, + } + provider := Kubernetes{ + Namespaces: []string{"awesome", "somewhat-awesome"}, + } + 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{ + "1": { + URL: "http://10.0.0.1:801", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + "bar": { + Servers: map[string]types.Server{ + "2": { + URL: "http://10.0.0.2:802", + Weight: 1, + }, + "3": { + URL: "https://10.0.0.3:443", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + "awesome/quix": { + Servers: map[string]types.Server{ + "1": { + URL: "http://10.0.0.1:801", + Weight: 1, + }, + }, + CircuitBreaker: nil, + LoadBalancer: nil, + }, + }, + Frontends: map[string]*types.Frontend{ + "foo/bar": { + Backend: "foo/bar", + Routes: map[string]types.Route{ + "/bar": { + Rule: "PathPrefixStrip:/bar", + }, + "foo": { + Rule: "Host:foo", + }, + }, + }, + "bar": { + Backend: "bar", + Routes: map[string]types.Route{ + "bar": { + Rule: "Host:bar", + }, + }, + }, + "awesome/quix": { + Backend: "awesome/quix", + Routes: map[string]types.Route{ + "/quix": { + Rule: "PathPrefixStrip:/quix", + }, + "awesome": { + Rule: "Host:awesome", + }, + }, + }, + }, + } + 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 []k8s.Ingress services []k8s.Service @@ -174,7 +602,13 @@ type clientMock struct { } func (c clientMock) GetIngresses(predicate func(k8s.Ingress) bool) ([]k8s.Ingress, error) { - return c.ingresses, nil + var ingresses []k8s.Ingress + for _, ingress := range c.ingresses { + if predicate(ingress) { + ingresses = append(ingresses, ingress) + } + } + return ingresses, nil } func (c clientMock) WatchIngresses(predicate func(k8s.Ingress) bool, stopCh <-chan bool) (chan interface{}, chan error, error) { return c.watchChan, make(chan error), nil diff --git a/traefik.sample.toml b/traefik.sample.toml index 4275479e1..51f2442e4 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -343,6 +343,7 @@ # Optional # # endpoint = "http://localhost:8080" +# namespaces = ["default"] ################################################################ # Consul KV configuration backend