From 04dd63da1c98b90f7d78a4f6421f4cd758d741cd Mon Sep 17 00:00:00 2001 From: Fernandez Ludovic Date: Sat, 2 Dec 2017 19:28:11 +0100 Subject: [PATCH] refactor(k8s): rewrite configuration system. --- provider/kubernetes/annotations_parser.go | 71 -------------- provider/kubernetes/client.go | 2 +- provider/kubernetes/client_mock_test.go | 66 +++++++++++++ provider/kubernetes/kubernetes.go | 110 +++++++++------------- provider/kubernetes/kubernetes_test.go | 93 ++++-------------- 5 files changed, 128 insertions(+), 214 deletions(-) delete mode 100644 provider/kubernetes/annotations_parser.go create mode 100644 provider/kubernetes/client_mock_test.go diff --git a/provider/kubernetes/annotations_parser.go b/provider/kubernetes/annotations_parser.go deleted file mode 100644 index 813146e6f..000000000 --- a/provider/kubernetes/annotations_parser.go +++ /dev/null @@ -1,71 +0,0 @@ -package kubernetes - -import ( - "net/http" - "strings" - - "github.com/containous/traefik/log" - "github.com/containous/traefik/types" - "k8s.io/client-go/pkg/apis/extensions/v1beta1" -) - -func getBoolAnnotation(meta *v1beta1.Ingress, 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 %q for %q, falling back to %v", annotationStringValue, name, defaultValue) - } - return annotationValue -} - -func getStringAnnotation(meta *v1beta1.Ingress, name string) string { - value := meta.Annotations[name] - return value -} - -func getSliceAnnotation(meta *v1beta1.Ingress, name string) []string { - var value []string - if annotation, ok := meta.Annotations[name]; ok && annotation != "" { - value = types.SplitAndTrimString(annotation) - } - if len(value) == 0 { - log.Debugf("Could not load %v annotation, skipping...", name) - return nil - } - return value -} - -func getMapAnnotation(meta *v1beta1.Ingress, annotName string) map[string]string { - if values, ok := meta.Annotations[annotName]; ok { - - if len(values) == 0 { - log.Errorf("Missing value for annotation %q", annotName) - return nil - } - - mapValue := make(map[string]string) - for _, parts := range strings.Split(values, "||") { - pair := strings.SplitN(parts, ":", 2) - if len(pair) != 2 { - log.Warnf("Could not load %q: %v, skipping...", annotName, pair) - } else { - mapValue[http.CanonicalHeaderKey(strings.TrimSpace(pair[0]))] = strings.TrimSpace(pair[1]) - } - } - - if len(mapValue) == 0 { - log.Errorf("Could not load %q, skipping...", annotName) - return nil - } - return mapValue - } - - return nil -} diff --git a/provider/kubernetes/client.go b/provider/kubernetes/client.go index d1aaa0f15..963bd639a 100644 --- a/provider/kubernetes/client.go +++ b/provider/kubernetes/client.go @@ -213,7 +213,7 @@ func (c *clientImpl) WatchObjects(namespace, kind string, object runtime.Object, return informer } -func loadInformer(listWatch *cache.ListWatch, object runtime.Object, watchCh chan<- interface{}) cache.SharedInformer { +func loadInformer(listWatch cache.ListerWatcher, object runtime.Object, watchCh chan<- interface{}) cache.SharedInformer { informer := cache.NewSharedInformer( listWatch, object, diff --git a/provider/kubernetes/client_mock_test.go b/provider/kubernetes/client_mock_test.go new file mode 100644 index 000000000..55acf1e2e --- /dev/null +++ b/provider/kubernetes/client_mock_test.go @@ -0,0 +1,66 @@ +package kubernetes + +import ( + "k8s.io/client-go/pkg/api/v1" + "k8s.io/client-go/pkg/apis/extensions/v1beta1" +) + +type clientMock struct { + ingresses []*v1beta1.Ingress + services []*v1.Service + secrets []*v1.Secret + endpoints []*v1.Endpoints + watchChan chan interface{} + + apiServiceError error + apiSecretError error + apiEndpointsError error +} + +func (c clientMock) GetIngresses() []*v1beta1.Ingress { + return c.ingresses +} + +func (c clientMock) GetService(namespace, name string) (*v1.Service, bool, error) { + if c.apiServiceError != nil { + return nil, false, c.apiServiceError + } + + for _, service := range c.services { + if service.Namespace == namespace && service.Name == name { + return service, true, nil + } + } + return nil, false, nil +} + +func (c clientMock) GetEndpoints(namespace, name string) (*v1.Endpoints, bool, error) { + if c.apiEndpointsError != nil { + return nil, false, c.apiEndpointsError + } + + for _, endpoints := range c.endpoints { + if endpoints.Namespace == namespace && endpoints.Name == name { + return endpoints, true, nil + } + } + + return &v1.Endpoints{}, false, nil +} + +func (c clientMock) GetSecret(namespace, name string) (*v1.Secret, bool, error) { + if c.apiSecretError != nil { + return nil, false, c.apiSecretError + } + + for _, secret := range c.secrets { + if secret.Namespace == namespace && secret.Name == name { + return secret, true, nil + } + } + return nil, false, nil +} + +func (c clientMock) WatchAll(namespaces Namespaces, labelString string, stopCh <-chan struct{}) (<-chan interface{}, error) { + return c.watchChan, nil +} diff --git a/provider/kubernetes/kubernetes.go b/provider/kubernetes/kubernetes.go index 804510d73..2d4fdebc7 100644 --- a/provider/kubernetes/kubernetes.go +++ b/provider/kubernetes/kubernetes.go @@ -17,6 +17,7 @@ import ( "github.com/containous/traefik/job" "github.com/containous/traefik/log" "github.com/containous/traefik/provider" + "github.com/containous/traefik/provider/label" "github.com/containous/traefik/safe" "github.com/containous/traefik/types" "k8s.io/client-go/pkg/api/v1" @@ -95,7 +96,10 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s // certain kinds of API errors getting logged into a directory not // available in a `FROM scratch` Docker container, causing glog to abort // hard with an exit code > 0. - flag.Set("logtostderr", "true") + err := flag.Set("logtostderr", "true") + if err != nil { + return err + } k8sClient, err := p.newK8sClient() if err != nil { @@ -186,8 +190,8 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) } } - passHostHeader := getBoolAnnotation(i, types.LabelFrontendPassHostHeader, !p.DisablePassHostHeaders) - passTLSCert := getBoolAnnotation(i, types.LabelFrontendPassTLSCert, p.EnablePassTLSCert) + passHostHeader := label.GetBoolValue(i.Annotations, label.TraefikFrontendPassHostHeader, !p.DisablePassHostHeaders) + passTLSCert := label.GetBoolValue(i.Annotations, label.TraefikFrontendPassTLSCert, p.EnablePassTLSCert) 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) @@ -195,11 +199,11 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) continue } - entryPoints := getSliceAnnotation(i, types.LabelFrontendEntryPoints) + entryPoints := label.GetSliceStringValue(i.Annotations, label.TraefikFrontendEntryPoints) - whitelistSourceRange := getSliceAnnotation(i, annotationKubernetesWhitelistSourceRange) + whitelistSourceRange := label.GetSliceStringValue(i.Annotations, annotationKubernetesWhitelistSourceRange) - entryPointRedirect, _ := i.Annotations[types.LabelFrontendRedirect] + entryPointRedirect := i.Annotations[label.TraefikFrontendRedirect] if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists { basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient) @@ -208,29 +212,29 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) continue } - priority := getPriority(i) + priority := label.GetIntValue(i.Annotations, label.TraefikFrontendPriority, 0) headers := types.Headers{ - CustomRequestHeaders: getMapAnnotation(i, annotationKubernetesCustomRequestHeaders), - CustomResponseHeaders: getMapAnnotation(i, annotationKubernetesCustomResponseHeaders), - AllowedHosts: getSliceAnnotation(i, annotationKubernetesAllowedHosts), - HostsProxyHeaders: getSliceAnnotation(i, annotationKubernetesProxyHeaders), - SSLRedirect: getBoolAnnotation(i, annotationKubernetesSSLRedirect, false), - SSLTemporaryRedirect: getBoolAnnotation(i, annotationKubernetesSSLTemporaryRedirect, false), - SSLHost: getStringAnnotation(i, annotationKubernetesSSLHost), - SSLProxyHeaders: getMapAnnotation(i, annotationKubernetesSSLProxyHeaders), - STSSeconds: getSTSSeconds(i), - STSIncludeSubdomains: getBoolAnnotation(i, annotationKubernetesHSTSIncludeSubdomains, false), - STSPreload: getBoolAnnotation(i, annotationKubernetesHSTSPreload, false), - ForceSTSHeader: getBoolAnnotation(i, annotationKubernetesForceHSTSHeader, false), - FrameDeny: getBoolAnnotation(i, annotationKubernetesFrameDeny, false), - CustomFrameOptionsValue: getStringAnnotation(i, annotationKubernetesCustomFrameOptionsValue), - ContentTypeNosniff: getBoolAnnotation(i, annotationKubernetesContentTypeNosniff, false), - BrowserXSSFilter: getBoolAnnotation(i, annotationKubernetesBrowserXSSFilter, false), - ContentSecurityPolicy: getStringAnnotation(i, annotationKubernetesContentSecurityPolicy), - PublicKey: getStringAnnotation(i, annotationKubernetesPublicKey), - ReferrerPolicy: getStringAnnotation(i, annotationKubernetesReferrerPolicy), - IsDevelopment: getBoolAnnotation(i, annotationKubernetesIsDevelopment, false), + CustomRequestHeaders: label.GetMapValue(i.Annotations, annotationKubernetesCustomRequestHeaders), + CustomResponseHeaders: label.GetMapValue(i.Annotations, annotationKubernetesCustomResponseHeaders), + AllowedHosts: label.GetSliceStringValue(i.Annotations, annotationKubernetesAllowedHosts), + HostsProxyHeaders: label.GetSliceStringValue(i.Annotations, annotationKubernetesProxyHeaders), + SSLRedirect: label.GetBoolValue(i.Annotations, annotationKubernetesSSLRedirect, false), + SSLTemporaryRedirect: label.GetBoolValue(i.Annotations, annotationKubernetesSSLTemporaryRedirect, false), + SSLHost: label.GetStringValue(i.Annotations, annotationKubernetesSSLHost, ""), + SSLProxyHeaders: label.GetMapValue(i.Annotations, annotationKubernetesSSLProxyHeaders), + STSSeconds: label.GetInt64Value(i.Annotations, annotationKubernetesHSTSMaxAge, 0), + STSIncludeSubdomains: label.GetBoolValue(i.Annotations, annotationKubernetesHSTSIncludeSubdomains, false), + STSPreload: label.GetBoolValue(i.Annotations, annotationKubernetesHSTSPreload, false), + ForceSTSHeader: label.GetBoolValue(i.Annotations, annotationKubernetesForceHSTSHeader, false), + FrameDeny: label.GetBoolValue(i.Annotations, annotationKubernetesFrameDeny, false), + CustomFrameOptionsValue: label.GetStringValue(i.Annotations, annotationKubernetesCustomFrameOptionsValue, ""), + ContentTypeNosniff: label.GetBoolValue(i.Annotations, annotationKubernetesContentTypeNosniff, false), + BrowserXSSFilter: label.GetBoolValue(i.Annotations, annotationKubernetesBrowserXSSFilter, false), + ContentSecurityPolicy: label.GetStringValue(i.Annotations, annotationKubernetesContentSecurityPolicy, ""), + PublicKey: label.GetStringValue(i.Annotations, annotationKubernetesPublicKey, ""), + ReferrerPolicy: label.GetStringValue(i.Annotations, annotationKubernetesReferrerPolicy, ""), + IsDevelopment: label.GetBoolValue(i.Annotations, annotationKubernetesIsDevelopment, false), } templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{ @@ -279,29 +283,29 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error) continue } - if expression := service.Annotations[types.LabelTraefikBackendCircuitbreaker]; expression != "" { + if expression := service.Annotations[label.TraefikBackendCircuitBreaker]; expression != "" { templateObjects.Backends[r.Host+pa.Path].CircuitBreaker = &types.CircuitBreaker{ Expression: expression, } } - if service.Annotations[types.LabelBackendLoadbalancerMethod] == "drr" { + if service.Annotations[label.TraefikBackendLoadBalancerMethod] == "drr" { templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Method = "drr" } - if sticky := service.Annotations[types.LabelBackendLoadbalancerSticky]; len(sticky) > 0 { - log.Warnf("Deprecated configuration found: %s. Please use %s.", types.LabelBackendLoadbalancerSticky, types.LabelBackendLoadbalancerStickiness) + if sticky := service.Annotations[label.TraefikBackendLoadBalancerSticky]; len(sticky) > 0 { + log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Sticky = strings.EqualFold(strings.TrimSpace(sticky), "true") } - if service.Annotations[types.LabelBackendLoadbalancerStickiness] == "true" { + if service.Annotations[label.TraefikBackendLoadBalancerStickiness] == "true" { templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Stickiness = &types.Stickiness{} - if cookieName := service.Annotations[types.LabelBackendLoadbalancerStickinessCookieName]; len(cookieName) > 0 { + if cookieName := service.Annotations[label.TraefikBackendLoadBalancerStickinessCookieName]; len(cookieName) > 0 { templateObjects.Backends[r.Host+pa.Path].LoadBalancer.Stickiness.CookieName = cookieName } } - protocol := "http" + protocol := label.DefaultProtocol for _, port := range service.Spec.Ports { if equalPorts(port, pa.Backend.ServicePort) { if port.Port == 443 { @@ -365,20 +369,12 @@ func (p *Provider) loadConfig(templateObjects types.Configuration) *types.Config return configuration } -func getSTSSeconds(i *v1beta1.Ingress) int64 { - value, err := strconv.ParseInt(i.ObjectMeta.Annotations[annotationKubernetesHSTSMaxAge], 10, 64) - if err == nil && value > 0 { - return value - } - return 0 -} - func getRuleForPath(pa v1beta1.HTTPIngressPath, i *v1beta1.Ingress) string { if len(pa.Path) == 0 { return "" } - ruleType := i.Annotations[types.LabelFrontendRuleType] + ruleType := i.Annotations[label.TraefikFrontendRuleType] if ruleType == "" { ruleType = ruleTypePathPrefix } @@ -392,39 +388,26 @@ func getRuleForPath(pa v1beta1.HTTPIngressPath, i *v1beta1.Ingress) string { return strings.Join(rules, ";") } -func getPriority(i *v1beta1.Ingress) int { - priority := 0 - - priorityRaw, ok := i.Annotations[types.LabelFrontendPriority] - if ok { - priorityParsed, err := strconv.Atoi(priorityRaw) - - if err == nil { - priority = priorityParsed - } else { - log.Errorf("Error in ingress: failed to parse %q value %q.", types.LabelFrontendPriority, priorityRaw) - } - } - - return priority -} - func handleBasicAuthConfig(i *v1beta1.Ingress, k8sClient Client) ([]string, error) { authType, exists := i.Annotations[annotationKubernetesAuthType] if !exists { return nil, nil } + if strings.ToLower(authType) != "basic" { return nil, fmt.Errorf("unsupported auth-type on annotation ingress.kubernetes.io/auth-type: %q", authType) } + authSecret := i.Annotations[annotationKubernetesAuthSecret] if authSecret == "" { return nil, errors.New("auth-secret annotation ingress.kubernetes.io/auth-secret must be set") } + basicAuthCreds, err := loadAuthCredentials(i.Namespace, authSecret, k8sClient) if err != nil { return nil, fmt.Errorf("failed to load auth credentials: %s", err) } + return basicAuthCreds, nil } @@ -485,10 +468,5 @@ func equalPorts(servicePort v1.ServicePort, ingressPort intstr.IntOrString) bool } func shouldProcessIngress(ingressClass string) bool { - switch ingressClass { - case "", "traefik": - return true - default: - return false - } + return ingressClass == "" || ingressClass == "traefik" } diff --git a/provider/kubernetes/kubernetes_test.go b/provider/kubernetes/kubernetes_test.go index df98a89b4..f62c8c404 100644 --- a/provider/kubernetes/kubernetes_test.go +++ b/provider/kubernetes/kubernetes_test.go @@ -6,6 +6,7 @@ import ( "reflect" "testing" + "github.com/containous/traefik/provider/label" "github.com/containous/traefik/types" "github.com/stretchr/testify/assert" "k8s.io/client-go/pkg/api/v1" @@ -372,7 +373,7 @@ func TestRuleType(t *testing.T) { if test.ingressRuleType != "" { ingress.ObjectMeta.Annotations = map[string]string{ - types.LabelFrontendRuleType: test.ingressRuleType, + label.TraefikFrontendRuleType: test.ingressRuleType, } } @@ -821,8 +822,8 @@ func TestServiceAnnotations(t *testing.T) { UID: "1", Namespace: "testing", Annotations: map[string]string{ - types.LabelTraefikBackendCircuitbreaker: "NetworkErrorRatio() > 0.5", - types.LabelBackendLoadbalancerMethod: "drr", + label.TraefikBackendCircuitBreaker: "NetworkErrorRatio() > 0.5", + label.TraefikBackendLoadBalancerMethod: "drr", }, }, Spec: v1.ServiceSpec{ @@ -840,8 +841,8 @@ func TestServiceAnnotations(t *testing.T) { UID: "2", Namespace: "testing", Annotations: map[string]string{ - types.LabelTraefikBackendCircuitbreaker: "", - types.LabelBackendLoadbalancerSticky: "true", + label.TraefikBackendCircuitBreaker: "", + label.TraefikBackendLoadBalancerSticky: "true", }, }, Spec: v1.ServiceSpec{ @@ -1009,7 +1010,7 @@ func TestIngressAnnotations(t *testing.T) { ObjectMeta: v1.ObjectMeta{ Namespace: "testing", Annotations: map[string]string{ - types.LabelFrontendPassHostHeader: "false", + label.TraefikFrontendPassHostHeader: "false", }, }, Spec: v1beta1.IngressSpec{ @@ -1037,8 +1038,8 @@ func TestIngressAnnotations(t *testing.T) { ObjectMeta: v1.ObjectMeta{ Namespace: "testing", Annotations: map[string]string{ - "kubernetes.io/ingress.class": "traefik", - types.LabelFrontendPassHostHeader: "true", + "kubernetes.io/ingress.class": "traefik", + label.TraefikFrontendPassHostHeader: "true", }, }, Spec: v1beta1.IngressSpec{ @@ -1066,8 +1067,8 @@ func TestIngressAnnotations(t *testing.T) { ObjectMeta: v1.ObjectMeta{ Namespace: "testing", Annotations: map[string]string{ - "kubernetes.io/ingress.class": "traefik", - types.LabelFrontendPassTLSCert: "true", + "kubernetes.io/ingress.class": "traefik", + label.TraefikFrontendPassTLSCert: "true", }, }, Spec: v1beta1.IngressSpec{ @@ -1095,8 +1096,8 @@ func TestIngressAnnotations(t *testing.T) { ObjectMeta: v1.ObjectMeta{ Namespace: "testing", Annotations: map[string]string{ - "kubernetes.io/ingress.class": "traefik", - types.LabelFrontendEntryPoints: "http,https", + "kubernetes.io/ingress.class": "traefik", + label.TraefikFrontendEntryPoints: "http,https", }, }, Spec: v1beta1.IngressSpec{ @@ -1267,7 +1268,7 @@ func TestIngressAnnotations(t *testing.T) { Namespace: "testing", Annotations: map[string]string{ "kubernetes.io/ingress.class": "traefik", - types.LabelFrontendRedirect: "https", + label.TraefikFrontendRedirect: "https", }, }, Spec: v1beta1.IngressSpec{ @@ -1563,7 +1564,7 @@ func TestPriorityHeaderValue(t *testing.T) { ObjectMeta: v1.ObjectMeta{ Namespace: "testing", Annotations: map[string]string{ - types.LabelFrontendPriority: "1337", + label.TraefikFrontendPriority: "1337", }, }, Spec: v1beta1.IngressSpec{ @@ -1664,7 +1665,7 @@ func TestInvalidPassTLSCertValue(t *testing.T) { ObjectMeta: v1.ObjectMeta{ Namespace: "testing", Annotations: map[string]string{ - types.LabelFrontendPassTLSCert: "herpderp", + label.TraefikFrontendPassTLSCert: "herpderp", }, }, Spec: v1beta1.IngressSpec{ @@ -1765,7 +1766,7 @@ func TestInvalidPassHostHeaderValue(t *testing.T) { ObjectMeta: v1.ObjectMeta{ Namespace: "testing", Annotations: map[string]string{ - types.LabelFrontendPassHostHeader: "herpderp", + label.TraefikFrontendPassHostHeader: "herpderp", }, }, Spec: v1beta1.IngressSpec{ @@ -2261,63 +2262,3 @@ func TestBasicAuthInTemplate(t *testing.T) { t.Fatalf("unexpected credentials: %+v", got) } } - -type clientMock struct { - ingresses []*v1beta1.Ingress - services []*v1.Service - secrets []*v1.Secret - endpoints []*v1.Endpoints - watchChan chan interface{} - - apiServiceError error - apiSecretError error - apiEndpointsError error -} - -func (c clientMock) GetIngresses() []*v1beta1.Ingress { - return c.ingresses -} - -func (c clientMock) GetService(namespace, name string) (*v1.Service, bool, error) { - if c.apiServiceError != nil { - return nil, false, c.apiServiceError - } - - for _, service := range c.services { - if service.Namespace == namespace && service.Name == name { - return service, true, nil - } - } - return nil, false, nil -} - -func (c clientMock) GetEndpoints(namespace, name string) (*v1.Endpoints, bool, error) { - if c.apiEndpointsError != nil { - return nil, false, c.apiEndpointsError - } - - for _, endpoints := range c.endpoints { - if endpoints.Namespace == namespace && endpoints.Name == name { - return endpoints, true, nil - } - } - - return &v1.Endpoints{}, false, nil -} - -func (c clientMock) GetSecret(namespace, name string) (*v1.Secret, bool, error) { - if c.apiSecretError != nil { - return nil, false, c.apiSecretError - } - - for _, secret := range c.secrets { - if secret.Namespace == namespace && secret.Name == name { - return secret, true, nil - } - } - return nil, false, nil -} - -func (c clientMock) WatchAll(namespaces Namespaces, labelString string, stopCh <-chan struct{}) (<-chan interface{}, error) { - return c.watchChan, nil -}