From 0d57e2aed93662fb6ea4674909bd9276dc0e9d4c Mon Sep 17 00:00:00 2001 From: Ludovic Fernandez Date: Wed, 10 Jan 2018 18:08:03 +0100 Subject: [PATCH] homogenization of templates: Rancher --- autogen/gentemplates/gen.go | 213 ++++---- provider/rancher/config.go | 287 +++++++---- provider/rancher/config_test.go | 884 ++++++++++++++++++++++++++------ templates/rancher.tmpl | 213 ++++---- 4 files changed, 1156 insertions(+), 441 deletions(-) diff --git a/autogen/gentemplates/gen.go b/autogen/gentemplates/gen.go index e4b53f14e..09b5f32f8 100644 --- a/autogen/gentemplates/gen.go +++ b/autogen/gentemplates/gen.go @@ -1327,147 +1327,162 @@ func templatesNotfoundTmpl() (*asset, error) { return a, nil } -var _templatesRancherTmpl = []byte(`{{$backendServers := .Backends}} +var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }} [backends] -{{range $backendName, $backend := .Backends}} +{{range $backendName, $backend := .Backends }} - [backends.backend-{{$backendName}}] + [backends.backend-{{ $backendName }}] - {{if hasCircuitBreakerLabel $backend}} - [backends.backend-{{$backendName}}.circuitBreaker] - expression = "{{getCircuitBreakerExpression $backend}}" + {{ $circuitBreaker := getCircuitBreaker $backend }} + {{if $circuitBreaker }} + [backends."backend-{{ $backendName }}".circuitBreaker] + expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{if hasLoadBalancerLabel $backend}} - [backends.backend-{{$backendName}}.loadBalancer] - method = "{{getLoadBalancerMethod $backend}}" - sticky = {{getSticky $backend}} - {{if hasStickinessLabel $backend}} - [backends.backend-{{$backendName}}.loadBalancer.stickiness] - cookieName = "{{getStickinessCookieName $backend}}" - {{end}} + {{ $loadBalancer := getLoadBalancer $backend }} + {{if $loadBalancer }} + [backends."backend-{{ $backendName }}".loadBalancer] + method = "{{ $loadBalancer.Method }}" + sticky = {{ $loadBalancer.Sticky }} + {{if $loadBalancer.Stickiness }} + [backends."backend-{{ $backendName }}".loadBalancer.stickiness] + cookieName = "{{ $loadBalancer.Stickiness.CookieName }}" + {{end}} {{end}} - {{if hasMaxConnLabels $backend}} - [backends.backend-{{$backendName}}.maxConn] - amount = {{getMaxConnAmount $backend}} - extractorFunc = "{{getMaxConnExtractorFunc $backend}}" + {{ $maxConn := getMaxConn $backend }} + {{if $maxConn }} + [backends."backend-{{ $backendName }}".maxConn] + extractorFunc = "{{ $maxConn.ExtractorFunc }}" + amount = {{ $maxConn.Amount }} {{end}} - {{if hasHealthCheckLabels $backend}} - [backends.backend-{{$backendName}}.healthCheck] - path = "{{getHealthCheckPath $backend}}" - port = {{getHealthCheckPort $backend}} - interval = "{{getHealthCheckInterval $backend}}" + {{ $healthCheck := getHealthCheck $backend }} + {{if $healthCheck }} + [backends.backend-{{ $backendName }}.healthCheck] + path = "{{ $healthCheck.Path }}" + port = {{ $healthCheck.Port }} + interval = "{{ $healthCheck.Interval }}" {{end}} - {{range $index, $ip := $backend.Containers}} - [backends.backend-{{$backendName}}.servers.server-{{$index}}] - url = "{{getProtocol $backend}}://{{$ip}}:{{getPort $backend}}" - weight = {{getWeight $backend}} + {{range $serverName, $server := getServers $backend}} + [backends.backend-{{ $backendName }}.servers.{{ $serverName }}] + url = "{{ $server.URL }}" + weight = {{ $server.Weight }} {{end}} {{end}} [frontends] -{{range $frontendName, $service := .Frontends}} +{{range $frontendName, $service := .Frontends }} - [frontends."frontend-{{$frontendName}}"] - backend = "backend-{{getBackend $service}}" - priority = {{getPriority $service}} - passHostHeader = {{getPassHostHeader $service}} - passTLSCert = {{getPassTLSCert $service}} + [frontends."frontend-{{ $frontendName }}"] + backend = "backend-{{ getBackendName $service }}" + priority = {{ getPriority $service }} + passHostHeader = {{ getPassHostHeader $service }} + passTLSCert = {{ getPassTLSCert $service }} - entryPoints = [{{range getEntryPoints $service}} + entryPoints = [{{range getEntryPoints $service }} "{{.}}", {{end}}] - {{if getWhitelistSourceRange $service}} - whitelistSourceRange = [{{range getWhitelistSourceRange $service}} + {{ $whitelistSourceRange := getWhitelistSourceRange $service }} + {{if $whitelistSourceRange }} + whitelistSourceRange = [{{range $whitelistSourceRange }} "{{.}}", {{end}}] {{end}} - basicAuth = [{{range getBasicAuth $service}} + basicAuth = [{{range getBasicAuth $service }} "{{.}}", {{end}}] - {{if hasRedirect $service}} - [frontends."frontend-{{$frontendName}}".redirect] - entryPoint = "{{getRedirectEntryPoint $service}}" - regex = "{{getRedirectRegex $service}}" - replacement = "{{getRedirectReplacement $service}}" + {{ $redirect := getRedirect $service }} + {{if $redirect }} + [frontends."frontend-{{ $frontendName }}".redirect] + entryPoint = "{{ $redirect.EntryPoint }}" + regex = "{{ $redirect.Regex }}" + replacement = "{{ $redirect.Replacement }}" {{end}} - {{ if hasErrorPages $service }} - [frontends."frontend-{{$frontendName}}".errors] - {{ range $pageName, $page := getErrorPages $service }} - [frontends."frontend-{{$frontendName}}".errors.{{ $pageName }}] - status = [{{range $page.Status}} + {{ $errorPages := getErrorPages $service }} + {{if $errorPages }} + [frontends."frontend-{{ $frontendName }}".errors] + {{range $pageName, $page := $errorPages }} + [frontends."frontend-{{ $frontendName }}".errors.{{ $pageName }}] + status = [{{range $page.Status }} "{{.}}", {{end}}] - backend = "{{$page.Backend}}" - query = "{{$page.Query}}" + backend = "{{ $page.Backend }}" + query = "{{ $page.Query }}" {{end}} {{end}} - {{ if hasRateLimits $service }} - [frontends."frontend-{{$frontendName}}".rateLimit] - extractorFunc = "{{ getRateLimitsExtractorFunc $service }}" - [frontends."frontend-{{$frontendName}}".rateLimit.rateSet] - {{ range $limitName, $rateLimit := getRateLimits $service }} - [frontends."frontend-{{$frontendName}}".rateLimit.rateSet.{{ $limitName }}] - period = "{{ $rateLimit.Period }}" - average = {{ $rateLimit.Average }} - burst = {{ $rateLimit.Burst }} - {{end}} + {{ $rateLimit := getRateLimit $service }} + {{if $rateLimit }} + [frontends."frontend-{{ $frontendName }}".rateLimit] + extractorFunc = "{{ $rateLimit.ExtractorFunc }}" + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet] + {{ range $limitName, $limit := $rateLimit.RateSet }} + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet.{{ $limitName }}] + period = "{{ $limit.Period }}" + average = {{ $limit.Average }} + burst = {{ $limit.Burst }} + {{end}} {{end}} - {{if hasHeaders $service }} - [frontends."frontend-{{$frontendName}}".headers] - SSLRedirect = {{getSSLRedirectHeaders $service}} - SSLTemporaryRedirect = {{getSSLTemporaryRedirectHeaders $service}} - SSLHost = "{{getSSLHostHeaders $service}}" - STSSeconds = {{getSTSSecondsHeaders $service}} - STSIncludeSubdomains = {{getSTSIncludeSubdomainsHeaders $service}} - STSPreload = {{getSTSPreloadHeaders $service}} - ForceSTSHeader = {{getForceSTSHeaderHeaders $service}} - FrameDeny = {{getFrameDenyHeaders $service}} - CustomFrameOptionsValue = "{{getCustomFrameOptionsValueHeaders $service}}" - ContentTypeNosniff = {{getContentTypeNosniffHeaders $service}} - BrowserXSSFilter = {{getBrowserXSSFilterHeaders $service}} - ContentSecurityPolicy = "{{getContentSecurityPolicyHeaders $service}}" - PublicKey = "{{getPublicKeyHeaders $service}}" - ReferrerPolicy = "{{getReferrerPolicyHeaders $service}}" - IsDevelopment = {{getIsDevelopmentHeaders $service}} + {{ $headers := getHeaders $service }} + {{if $headers }} + [frontends."frontend-{{ $frontendName }}".headers] + SSLRedirect = {{ $headers.SSLRedirect }} + SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }} + SSLHost = "{{ $headers.SSLHost }}" + STSSeconds = {{ $headers.STSSeconds }} + STSIncludeSubdomains = {{ $headers.STSIncludeSubdomains }} + STSPreload = {{ $headers.STSPreload }} + ForceSTSHeader = {{ $headers.ForceSTSHeader }} + FrameDeny = {{ $headers.FrameDeny }} + CustomFrameOptionsValue = "{{ $headers.CustomFrameOptionsValue }}" + ContentTypeNosniff = {{ $headers.ContentTypeNosniff }} + BrowserXSSFilter = {{ $headers.BrowserXSSFilter }} + ContentSecurityPolicy = "{{ $headers.ContentSecurityPolicy }}" + PublicKey = "{{ $headers.PublicKey }}" + ReferrerPolicy = "{{ $headers.ReferrerPolicy }}" + IsDevelopment = {{ $headers.IsDevelopment }} - AllowedHosts = [{{range getAllowedHostsHeaders $service}} - "{{.}}", - {{end}}] + {{if $headers.AllowedHosts }} + AllowedHosts = [{{range $headers.AllowedHosts }} + "{{.}}", + {{end}}] + {{end}} - HostsProxyHeaders = [{{range getHostsProxyHeaders $service}} - "{{.}}", - {{end}}] + {{if $headers.HostsProxyHeaders }} + HostsProxyHeaders = [{{range $headers.HostsProxyHeaders }} + "{{.}}", + {{end}}] + {{end}} - {{if hasRequestHeaders $service}} - [frontends."frontend-{{$frontendName}}".headers.customRequestHeaders] - {{range $k, $v := getRequestHeaders $service}} - {{$k}} = "{{$v}}" + {{if $headers.CustomRequestHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.customRequestHeaders] + {{range $k, $v := $headers.CustomRequestHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.CustomResponseHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.customResponseHeaders] + {{range $k, $v := $headers.CustomResponseHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.SSLProxyHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.SSLProxyHeaders] + {{range $k, $v := $headers.SSLProxyHeaders }} + {{$k}} = "{{$v}}" + {{end}} {{end}} {{end}} - {{if hasResponseHeaders $service}} - [frontends."frontend-{{$frontendName}}".headers.customResponseHeaders] - {{range $k, $v := getResponseHeaders $service}} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - {{if hasSSLProxyHeaders $service}} - [frontends."frontend-{{$frontendName}}".headers.SSLProxyHeaders] - {{range $k, $v := getSSLProxyHeaders $service}} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - {{end}} [frontends."frontend-{{$frontendName}}".routes."route-frontend-{{$frontendName}}"] rule = "{{getFrontendRule $service}}" diff --git a/provider/rancher/config.go b/provider/rancher/config.go index 01036a460..75410e3c3 100644 --- a/provider/rancher/config.go +++ b/provider/rancher/config.go @@ -1,7 +1,9 @@ package rancher import ( + "fmt" "math" + "strconv" "strings" "text/template" @@ -15,70 +17,57 @@ import ( func (p *Provider) buildConfiguration(services []rancherData) *types.Configuration { var RancherFuncMap = template.FuncMap{ - "getDomain": getFuncString(label.TraefikDomain, p.Domain), // FIXME dead ? + "getDomain": getFuncString(label.TraefikDomain, p.Domain), // Backend functions - "getPort": getFuncString(label.TraefikPort, ""), - "getProtocol": getFuncString(label.TraefikProtocol, label.DefaultProtocol), - "getWeight": getFuncString(label.TraefikWeight, label.DefaultWeight), - "hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression), + "getCircuitBreaker": getCircuitBreaker, + "getLoadBalancer": getLoadBalancer, + "getMaxConn": getMaxConn, + "getHealthCheck": getHealthCheck, + "getServers": getServers, + + // TODO Deprecated [breaking] + "getPort": getFuncString(label.TraefikPort, ""), + // TODO Deprecated [breaking] + "getProtocol": getFuncString(label.TraefikProtocol, label.DefaultProtocol), + // TODO Deprecated [breaking] + "getWeight": getFuncInt(label.TraefikWeight, label.DefaultWeightInt), + // TODO Deprecated [breaking] + "hasCircuitBreakerLabel": hasFunc(label.TraefikBackendCircuitBreakerExpression), + // TODO Deprecated [breaking] "getCircuitBreakerExpression": getFuncString(label.TraefikBackendCircuitBreakerExpression, label.DefaultCircuitBreakerExpression), - "hasLoadBalancerLabel": hasLoadBalancerLabel, - "getLoadBalancerMethod": getFuncString(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), - "hasMaxConnLabels": hasMaxConnLabels, - "getMaxConnAmount": getFuncInt64(label.TraefikBackendMaxConnAmount, math.MaxInt64), - "getMaxConnExtractorFunc": getFuncString(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), - "getSticky": getSticky, - "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), - "getStickinessCookieName": getFuncString(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), - "hasHealthCheckLabels": hasFunc(label.TraefikBackendHealthCheckPath), - "getHealthCheckPath": getFuncString(label.TraefikBackendHealthCheckPath, ""), - "getHealthCheckPort": getFuncInt(label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort), - "getHealthCheckInterval": getFuncString(label.TraefikBackendHealthCheckInterval, ""), + // TODO Deprecated [breaking] + "hasLoadBalancerLabel": hasLoadBalancerLabel, + // TODO Deprecated [breaking] + "getLoadBalancerMethod": getFuncString(label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod), + // TODO Deprecated [breaking] + "hasMaxConnLabels": hasMaxConnLabels, + // TODO Deprecated [breaking] + "getMaxConnAmount": getFuncInt64(label.TraefikBackendMaxConnAmount, 0), + // TODO Deprecated [breaking] + "getMaxConnExtractorFunc": getFuncString(label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc), + // TODO Deprecated [breaking] + "getSticky": getSticky, + // TODO Deprecated [breaking] + "hasStickinessLabel": hasFunc(label.TraefikBackendLoadBalancerStickiness), + // TODO Deprecated [breaking] + "getStickinessCookieName": getFuncString(label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName), // Frontend functions - "getBackend": getBackend, - "getPriority": getFuncString(label.TraefikFrontendPriority, label.DefaultFrontendPriority), - "getPassHostHeader": getFuncString(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeader), - "getPassTLSCert": getFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), - "getEntryPoints": getFuncSliceString(label.TraefikFrontendEntryPoints), - "getBasicAuth": getFuncSliceString(label.TraefikFrontendAuthBasic), - "getWhitelistSourceRange": getFuncSliceString(label.TraefikFrontendWhitelistSourceRange), - "getFrontendRule": p.getFrontendRule, - "hasRedirect": hasRedirect, - "getRedirectEntryPoint": getFuncString(label.TraefikFrontendRedirectEntryPoint, label.DefaultFrontendRedirectEntryPoint), - "getRedirectRegex": getFuncString(label.TraefikFrontendRedirectRegex, ""), - "getRedirectReplacement": getFuncString(label.TraefikFrontendRedirectReplacement, ""), - "hasErrorPages": hasPrefixFunc(label.Prefix + label.BaseFrontendErrorPage), - "getErrorPages": getErrorPages, - "hasRateLimits": hasFunc(label.TraefikFrontendRateLimitExtractorFunc), - "getRateLimitsExtractorFunc": getFuncString(label.TraefikFrontendRateLimitExtractorFunc, ""), - "getRateLimits": getRateLimits, - // Headers - "hasHeaders": hasPrefixFunc(label.TraefikFrontendHeaders), - "hasRequestHeaders": hasFunc(label.TraefikFrontendRequestHeaders), - "getRequestHeaders": getFuncMap(label.TraefikFrontendRequestHeaders), - "hasResponseHeaders": hasFunc(label.TraefikFrontendResponseHeaders), - "getResponseHeaders": getFuncMap(label.TraefikFrontendResponseHeaders), - "getAllowedHostsHeaders": getFuncSliceString(label.TraefikFrontendAllowedHosts), - "getHostsProxyHeaders": getFuncSliceString(label.TraefikFrontendHostsProxyHeaders), - "getSSLRedirectHeaders": getFuncBool(label.TraefikFrontendSSLRedirect, false), - "getSSLTemporaryRedirectHeaders": getFuncBool(label.TraefikFrontendSSLTemporaryRedirect, false), - "getSSLHostHeaders": getFuncString(label.TraefikFrontendSSLHost, ""), - "hasSSLProxyHeaders": hasFunc(label.TraefikFrontendSSLProxyHeaders), - "getSSLProxyHeaders": getFuncMap(label.TraefikFrontendSSLProxyHeaders), - "getSTSSecondsHeaders": getFuncInt64(label.TraefikFrontendSTSSeconds, 0), - "getSTSIncludeSubdomainsHeaders": getFuncBool(label.TraefikFrontendSTSIncludeSubdomains, false), - "getSTSPreloadHeaders": getFuncBool(label.TraefikFrontendSTSPreload, false), - "getForceSTSHeaderHeaders": getFuncBool(label.TraefikFrontendForceSTSHeader, false), - "getFrameDenyHeaders": getFuncBool(label.TraefikFrontendFrameDeny, false), - "getCustomFrameOptionsValueHeaders": getFuncString(label.TraefikFrontendCustomFrameOptionsValue, ""), - "getContentTypeNosniffHeaders": getFuncBool(label.TraefikFrontendContentTypeNosniff, false), - "getBrowserXSSFilterHeaders": getFuncBool(label.TraefikFrontendBrowserXSSFilter, false), - "getContentSecurityPolicyHeaders": getFuncString(label.TraefikFrontendContentSecurityPolicy, ""), - "getPublicKeyHeaders": getFuncString(label.TraefikFrontendPublicKey, ""), - "getReferrerPolicyHeaders": getFuncString(label.TraefikFrontendReferrerPolicy, ""), - "getIsDevelopmentHeaders": getFuncBool(label.TraefikFrontendIsDevelopment, false), + "getBackend": getBackendName, // TODO Deprecated [breaking] replaced by getBackendName + "getBackendName": getBackendName, + "getFrontendRule": p.getFrontendRule, + "getPriority": getFuncInt(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), + "getPassHostHeader": getFuncBool(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), + "getPassTLSCert": getFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), + "getEntryPoints": getFuncSliceString(label.TraefikFrontendEntryPoints), + "getBasicAuth": getFuncSliceString(label.TraefikFrontendAuthBasic), + "getWhitelistSourceRange": getFuncSliceString(label.TraefikFrontendWhitelistSourceRange), + + "getErrorPages": getErrorPages, + "getRateLimit": getRateLimit, + "getRedirect": getRedirect, + "getHeaders": getHeaders, } // filter services @@ -90,7 +79,7 @@ func (p *Provider) buildConfiguration(services []rancherData) *types.Configurati for _, service := range filteredServices { frontendName := p.getFrontendName(service) frontends[frontendName] = service - backendName := getBackend(service) + backendName := getBackendName(service) backends[backendName] = service } @@ -113,7 +102,6 @@ func (p *Provider) buildConfiguration(services []rancherData) *types.Configurati } func (p *Provider) serviceFilter(service rancherData) bool { - if service.Labels[label.TraefikPort] == "" { log.Debugf("Filtering service %s without traefik.port label", service.Name) return false @@ -161,13 +149,14 @@ func (p *Provider) getFrontendName(service rancherData) string { // TODO: Deprecated // replaced by Stickiness // Deprecated -func getSticky(service rancherData) string { +func getSticky(service rancherData) bool { if label.Has(service.Labels, label.TraefikBackendLoadBalancerSticky) { log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikBackendLoadBalancerSticky, label.TraefikBackendLoadBalancerStickiness) } - return label.GetStringValue(service.Labels, label.TraefikBackendLoadBalancerSticky, "false") + return label.GetBoolValue(service.Labels, label.TraefikBackendLoadBalancerSticky, false) } +// Deprecated func hasLoadBalancerLabel(service rancherData) bool { method := label.Has(service.Labels, label.TraefikBackendLoadBalancerMethod) sticky := label.Has(service.Labels, label.TraefikBackendLoadBalancerSticky) @@ -176,23 +165,114 @@ func hasLoadBalancerLabel(service rancherData) bool { return method || sticky || stickiness || cookieName } +// Deprecated func hasMaxConnLabels(service rancherData) bool { mca := label.Has(service.Labels, label.TraefikBackendMaxConnAmount) mcef := label.Has(service.Labels, label.TraefikBackendMaxConnExtractorFunc) return mca && mcef } -func getBackend(service rancherData) string { +func getBackendName(service rancherData) string { backend := label.GetStringValue(service.Labels, label.TraefikBackend, service.Name) return provider.Normalize(backend) } -func hasRedirect(service rancherData) bool { - frep := label.Has(service.Labels, label.TraefikFrontendRedirectEntryPoint) - frrg := label.Has(service.Labels, label.TraefikFrontendRedirectRegex) - frrp := label.Has(service.Labels, label.TraefikFrontendRedirectReplacement) +func getCircuitBreaker(service rancherData) *types.CircuitBreaker { + circuitBreaker := label.GetStringValue(service.Labels, label.TraefikBackendCircuitBreakerExpression, "") + if len(circuitBreaker) == 0 { + return nil + } + return &types.CircuitBreaker{Expression: circuitBreaker} +} - return frep || frrg && frrp +func getLoadBalancer(service rancherData) *types.LoadBalancer { + if !label.HasPrefix(service.Labels, label.TraefikBackendLoadBalancer) { + return nil + } + + method := label.GetStringValue(service.Labels, label.TraefikBackendLoadBalancerMethod, label.DefaultBackendLoadBalancerMethod) + + lb := &types.LoadBalancer{ + Method: method, + Sticky: getSticky(service), + } + + if label.GetBoolValue(service.Labels, label.TraefikBackendLoadBalancerStickiness, false) { + cookieName := label.GetStringValue(service.Labels, label.TraefikBackendLoadBalancerStickinessCookieName, label.DefaultBackendLoadbalancerStickinessCookieName) + lb.Stickiness = &types.Stickiness{CookieName: cookieName} + } + + return lb +} + +func getMaxConn(service rancherData) *types.MaxConn { + amount := label.GetInt64Value(service.Labels, label.TraefikBackendMaxConnAmount, math.MinInt64) + extractorFunc := label.GetStringValue(service.Labels, label.TraefikBackendMaxConnExtractorFunc, label.DefaultBackendMaxconnExtractorFunc) + + if amount == math.MinInt64 || len(extractorFunc) == 0 { + return nil + } + + return &types.MaxConn{ + Amount: amount, + ExtractorFunc: extractorFunc, + } +} + +func getHealthCheck(service rancherData) *types.HealthCheck { + path := label.GetStringValue(service.Labels, label.TraefikBackendHealthCheckPath, "") + if len(path) == 0 { + return nil + } + + port := label.GetIntValue(service.Labels, label.TraefikBackendHealthCheckPort, label.DefaultBackendHealthCheckPort) + interval := label.GetStringValue(service.Labels, label.TraefikBackendHealthCheckInterval, "") + + return &types.HealthCheck{ + Path: path, + Port: port, + Interval: interval, + } +} + +func getServers(service rancherData) map[string]types.Server { + var servers map[string]types.Server + + for index, ip := range service.Containers { + if servers == nil { + servers = make(map[string]types.Server) + } + + protocol := label.GetStringValue(service.Labels, label.TraefikProtocol, label.DefaultProtocol) + port := label.GetStringValue(service.Labels, label.TraefikPort, "") + weight := label.GetIntValue(service.Labels, label.TraefikWeight, label.DefaultWeightInt) + + serverName := "server-" + strconv.Itoa(index) + servers[serverName] = types.Server{ + URL: fmt.Sprintf("%s://%s:%s", protocol, ip, port), + Weight: weight, + } + } + + return servers +} + +func getRedirect(service rancherData) *types.Redirect { + if label.Has(service.Labels, label.TraefikFrontendRedirectEntryPoint) { + return &types.Redirect{ + EntryPoint: label.GetStringValue(service.Labels, label.TraefikFrontendRedirectEntryPoint, ""), + } + } + + if label.Has(service.Labels, label.TraefikFrontendRedirectRegex) && + label.Has(service.Labels, label.TraefikFrontendRedirectReplacement) { + return &types.Redirect{ + Regex: label.GetStringValue(service.Labels, label.TraefikFrontendRedirectRegex, ""), + Replacement: label.GetStringValue(service.Labels, label.TraefikFrontendRedirectReplacement, ""), + } + } + + return nil } func getErrorPages(service rancherData) map[string]*types.ErrorPage { @@ -200,9 +280,50 @@ func getErrorPages(service rancherData) map[string]*types.ErrorPage { return label.ParseErrorPages(service.Labels, prefix, label.RegexpFrontendErrorPage) } -func getRateLimits(service rancherData) map[string]*types.Rate { +func getRateLimit(service rancherData) *types.RateLimit { + extractorFunc := label.GetStringValue(service.Labels, label.TraefikFrontendRateLimitExtractorFunc, "") + if len(extractorFunc) == 0 { + return nil + } + prefix := label.Prefix + label.BaseFrontendRateLimit - return label.ParseRateSets(service.Labels, prefix, label.RegexpFrontendRateLimit) + limits := label.ParseRateSets(service.Labels, prefix, label.RegexpFrontendRateLimit) + + return &types.RateLimit{ + ExtractorFunc: extractorFunc, + RateSet: limits, + } +} + +func getHeaders(service rancherData) *types.Headers { + headers := &types.Headers{ + CustomRequestHeaders: label.GetMapValue(service.Labels, label.TraefikFrontendRequestHeaders), + CustomResponseHeaders: label.GetMapValue(service.Labels, label.TraefikFrontendResponseHeaders), + SSLProxyHeaders: label.GetMapValue(service.Labels, label.TraefikFrontendSSLProxyHeaders), + AllowedHosts: label.GetSliceStringValue(service.Labels, label.TraefikFrontendAllowedHosts), + HostsProxyHeaders: label.GetSliceStringValue(service.Labels, label.TraefikFrontendHostsProxyHeaders), + STSSeconds: label.GetInt64Value(service.Labels, label.TraefikFrontendSTSSeconds, 0), + SSLRedirect: label.GetBoolValue(service.Labels, label.TraefikFrontendSSLRedirect, false), + SSLTemporaryRedirect: label.GetBoolValue(service.Labels, label.TraefikFrontendSSLTemporaryRedirect, false), + STSIncludeSubdomains: label.GetBoolValue(service.Labels, label.TraefikFrontendSTSIncludeSubdomains, false), + STSPreload: label.GetBoolValue(service.Labels, label.TraefikFrontendSTSPreload, false), + ForceSTSHeader: label.GetBoolValue(service.Labels, label.TraefikFrontendForceSTSHeader, false), + FrameDeny: label.GetBoolValue(service.Labels, label.TraefikFrontendFrameDeny, false), + ContentTypeNosniff: label.GetBoolValue(service.Labels, label.TraefikFrontendContentTypeNosniff, false), + BrowserXSSFilter: label.GetBoolValue(service.Labels, label.TraefikFrontendBrowserXSSFilter, false), + IsDevelopment: label.GetBoolValue(service.Labels, label.TraefikFrontendIsDevelopment, false), + SSLHost: label.GetStringValue(service.Labels, label.TraefikFrontendSSLHost, ""), + CustomFrameOptionsValue: label.GetStringValue(service.Labels, label.TraefikFrontendCustomFrameOptionsValue, ""), + ContentSecurityPolicy: label.GetStringValue(service.Labels, label.TraefikFrontendContentSecurityPolicy, ""), + PublicKey: label.GetStringValue(service.Labels, label.TraefikFrontendPublicKey, ""), + ReferrerPolicy: label.GetStringValue(service.Labels, label.TraefikFrontendReferrerPolicy, ""), + } + + if !headers.HasSecureHeadersDefined() && !headers.HasCustomHeadersDefined() { + return nil + } + + return headers } // Label functions @@ -213,18 +334,18 @@ func getFuncString(labelName string, defaultValue string) func(service rancherDa } } -func getFuncInt(labelName string, defaultValue int) func(service rancherData) int { - return func(service rancherData) int { - return label.GetIntValue(service.Labels, labelName, defaultValue) - } -} - func getFuncBool(labelName string, defaultValue bool) func(service rancherData) bool { return func(service rancherData) bool { return label.GetBoolValue(service.Labels, labelName, defaultValue) } } +func getFuncInt(labelName string, defaultValue int) func(service rancherData) int { + return func(service rancherData) int { + return label.GetIntValue(service.Labels, labelName, defaultValue) + } +} + func getFuncInt64(labelName string, defaultValue int64) func(service rancherData) int64 { return func(service rancherData) int64 { return label.GetInt64Value(service.Labels, labelName, defaultValue) @@ -237,20 +358,8 @@ func getFuncSliceString(labelName string) func(service rancherData) []string { } } -func getFuncMap(labelName string) func(service rancherData) map[string]string { - return func(service rancherData) map[string]string { - return label.GetMapValue(service.Labels, labelName) - } -} - func hasFunc(labelName string) func(service rancherData) bool { return func(service rancherData) bool { return label.Has(service.Labels, labelName) } } - -func hasPrefixFunc(prefix string) func(service rancherData) bool { - return func(service rancherData) bool { - return label.HasPrefix(service.Labels, prefix) - } -} diff --git a/provider/rancher/config_test.go b/provider/rancher/config_test.go index a4ac04e15..1bb056303 100644 --- a/provider/rancher/config_test.go +++ b/provider/rancher/config_test.go @@ -29,6 +29,208 @@ func TestProviderBuildConfiguration(t *testing.T) { expectedFrontends: map[string]*types.Frontend{}, expectedBackends: map[string]*types.Backend{}, }, + { + desc: "when all labels are set", + services: []rancherData{ + { + Labels: map[string]string{ + label.TraefikPort: "666", + label.TraefikProtocol: "https", + label.TraefikWeight: "12", + + label.TraefikBackend: "foobar", + + label.TraefikBackendCircuitBreakerExpression: "NetworkErrorRatio() > 0.5", + label.TraefikBackendHealthCheckPath: "/health", + label.TraefikBackendHealthCheckPort: "880", + label.TraefikBackendHealthCheckInterval: "6", + label.TraefikBackendLoadBalancerMethod: "drr", + label.TraefikBackendLoadBalancerSticky: "true", + label.TraefikBackendLoadBalancerStickiness: "true", + label.TraefikBackendLoadBalancerStickinessCookieName: "chocolate", + label.TraefikBackendMaxConnAmount: "666", + label.TraefikBackendMaxConnExtractorFunc: "client.ip", + + label.TraefikFrontendAuthBasic: "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + label.TraefikFrontendEntryPoints: "http,https", + label.TraefikFrontendPassHostHeader: "true", + label.TraefikFrontendPassTLSCert: "true", + label.TraefikFrontendPriority: "666", + label.TraefikFrontendRedirectEntryPoint: "https", + label.TraefikFrontendRedirectRegex: "nope", + label.TraefikFrontendRedirectReplacement: "nope", + label.TraefikFrontendRule: "Host:traefik.io", + label.TraefikFrontendWhitelistSourceRange: "10.10.10.10", + + label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendAllowedHosts: "foo,bar,bor", + label.TraefikFrontendHostsProxyHeaders: "foo,bar,bor", + label.TraefikFrontendSSLHost: "foo", + label.TraefikFrontendCustomFrameOptionsValue: "foo", + label.TraefikFrontendContentSecurityPolicy: "foo", + label.TraefikFrontendPublicKey: "foo", + label.TraefikFrontendReferrerPolicy: "foo", + label.TraefikFrontendSTSSeconds: "666", + label.TraefikFrontendSSLRedirect: "true", + label.TraefikFrontendSSLTemporaryRedirect: "true", + label.TraefikFrontendSTSIncludeSubdomains: "true", + label.TraefikFrontendSTSPreload: "true", + label.TraefikFrontendForceSTSHeader: "true", + label.TraefikFrontendFrameDeny: "true", + label.TraefikFrontendContentTypeNosniff: "true", + label.TraefikFrontendBrowserXSSFilter: "true", + label.TraefikFrontendIsDevelopment: "true", + + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "foobar", + label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", + + label.TraefikFrontendRateLimitExtractorFunc: "client.ip", + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", + }, + Health: "healthy", + Containers: []string{"10.0.0.1", "10.0.0.2"}, + }, + }, + expectedFrontends: map[string]*types.Frontend{ + "frontend-Host-traefik-io": { + EntryPoints: []string{ + "http", + "https", + }, + Backend: "backend-foobar", + Routes: map[string]types.Route{ + "route-frontend-Host-traefik-io": { + Rule: "Host:traefik.io", + }, + }, + PassHostHeader: true, + PassTLSCert: true, + Priority: 666, + BasicAuth: []string{ + "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", + "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", + }, + WhitelistSourceRange: []string{ + "10.10.10.10", + }, + Headers: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{ + "foo", + "bar", + "bor", + }, + HostsProxyHeaders: []string{ + "foo", + "bar", + "bor", + }, + SSLRedirect: true, + SSLTemporaryRedirect: true, + SSLHost: "foo", + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + STSSeconds: 666, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + CustomFrameOptionsValue: "foo", + ContentTypeNosniff: true, + BrowserXSSFilter: true, + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + IsDevelopment: true, + }, + Errors: map[string]*types.ErrorPage{ + "foo": { + Status: []string{"404"}, + Query: "foo_query", + Backend: "foobar", + }, + "bar": { + Status: []string{"500", "600"}, + Query: "bar_query", + Backend: "foobar", + }, + }, + RateLimit: &types.RateLimit{ + ExtractorFunc: "client.ip", + RateSet: map[string]*types.Rate{ + "foo": { + Period: flaeg.Duration(6 * time.Second), + Average: 12, + Burst: 18, + }, + "bar": { + Period: flaeg.Duration(3 * time.Second), + Average: 6, + Burst: 9, + }, + }, + }, + Redirect: &types.Redirect{ + EntryPoint: "https", + Regex: "", + Replacement: "", + }, + }, + }, + expectedBackends: map[string]*types.Backend{ + "backend-foobar": { + Servers: map[string]types.Server{ + "server-0": { + URL: "https://10.0.0.1:666", + Weight: 12, + }, + "server-1": { + URL: "https://10.0.0.2:666", + Weight: 12, + }, + }, + CircuitBreaker: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", + }, + LoadBalancer: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "chocolate", + }, + }, + MaxConn: &types.MaxConn{ + Amount: 666, + ExtractorFunc: "client.ip", + }, + HealthCheck: &types.HealthCheck{ + Path: "/health", + Port: 880, + Interval: "6", + }, + }, + }, + }, { desc: "with services", services: []rancherData{ @@ -72,118 +274,6 @@ func TestProviderBuildConfiguration(t *testing.T) { }, }, }, - { - desc: "with Error Pages", - services: []rancherData{ - { - Name: "test/service", - Labels: map[string]string{ - label.TraefikPort: "80", - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageStatus: "404", - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageBackend: "foobar", - label.Prefix + label.BaseFrontendErrorPage + "foo." + label.SuffixErrorPageQuery: "foo_query", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageStatus: "500,600", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageBackend: "foobar", - label.Prefix + label.BaseFrontendErrorPage + "bar." + label.SuffixErrorPageQuery: "bar_query", - }, - Health: "healthy", - Containers: []string{"127.0.0.1"}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-test-service": { - Servers: map[string]types.Server{ - "server-0": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - }, - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test-service-rancher-localhost": { - EntryPoints: []string{}, - BasicAuth: []string{}, - Backend: "backend-test-service", - Routes: map[string]types.Route{ - "route-frontend-Host-test-service-rancher-localhost": { - Rule: "Host:test.service.rancher.localhost", - }, - }, - PassHostHeader: true, - Errors: map[string]*types.ErrorPage{ - "foo": { - Status: []string{"404"}, - Query: "foo_query", - Backend: "foobar", - }, - "bar": { - Status: []string{"500", "600"}, - Query: "bar_query", - Backend: "foobar", - }, - }, - }, - }, - }, - { - desc: "with rate Limits", - services: []rancherData{ - { - Name: "test/service", - Labels: map[string]string{ - label.TraefikPort: "80", - label.TraefikFrontendRateLimitExtractorFunc: "client.ip", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", - label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", - label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", - }, - Health: "healthy", - Containers: []string{"127.0.0.1"}, - }, - }, - expectedBackends: map[string]*types.Backend{ - "backend-test-service": { - Servers: map[string]types.Server{ - "server-0": { - URL: "http://127.0.0.1:80", - Weight: 0, - }, - }, - }, - }, - expectedFrontends: map[string]*types.Frontend{ - "frontend-Host-test-service-rancher-localhost": { - EntryPoints: []string{}, - BasicAuth: []string{}, - Backend: "backend-test-service", - Routes: map[string]types.Route{ - "route-frontend-Host-test-service-rancher-localhost": { - Rule: "Host:test.service.rancher.localhost", - }, - }, - PassHostHeader: true, - RateLimit: &types.RateLimit{ - ExtractorFunc: "client.ip", - RateSet: map[string]*types.Rate{ - "foo": { - Period: flaeg.Duration(6 * time.Second), - Average: 12, - Burst: 18, - }, - "bar": { - Period: flaeg.Duration(3 * time.Second), - Average: 6, - Burst: 9, - }, - }, - }, - }, - }, - }, } for _, test := range testCases { @@ -493,7 +583,7 @@ func TestProviderGetFrontendRule(t *testing.T) { } } -func TestGetBackend(t *testing.T) { +func TestGetBackendName(t *testing.T) { testCases := []struct { desc string service rancherData @@ -524,65 +614,39 @@ func TestGetBackend(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := getBackend(test.service) + actual := getBackendName(test.service) assert.Equal(t, test.expected, actual) }) } } -func TestHasRedirect(t *testing.T) { +func TestGetCircuitBreaker(t *testing.T) { testCases := []struct { desc string service rancherData - expected bool + expected *types.CircuitBreaker }{ { - desc: "without redirect labels", + desc: "should return nil when no CB label", service: rancherData{ - Name: "test-service", + Labels: map[string]string{}, + Health: "healthy", + State: "active", }, - expected: false, + expected: nil, }, { - desc: "with Redirect EntryPoint label", + desc: "should return a struct when CB label is set", service: rancherData{ - Name: "test-service", Labels: map[string]string{ - label.TraefikFrontendRedirectEntryPoint: "https", + label.TraefikBackendCircuitBreakerExpression: "NetworkErrorRatio() > 0.5", }, + Health: "healthy", + State: "active", }, - expected: true, - }, - { - desc: "with Redirect regex label", - service: rancherData{ - Name: "test-service", - Labels: map[string]string{ - label.TraefikFrontendRedirectRegex: `(.+)`, - }, + expected: &types.CircuitBreaker{ + Expression: "NetworkErrorRatio() > 0.5", }, - expected: false, - }, - { - desc: "with Redirect replacement label", - service: rancherData{ - Name: "test-service", - Labels: map[string]string{ - label.TraefikFrontendRedirectReplacement: "$1", - }, - }, - expected: false, - }, - { - desc: "with Redirect regex & replacement labels", - service: rancherData{ - Name: "test-service", - Labels: map[string]string{ - label.TraefikFrontendRedirectRegex: `(.+)`, - label.TraefikFrontendRedirectReplacement: "$1", - }, - }, - expected: true, }, } @@ -591,7 +655,519 @@ func TestHasRedirect(t *testing.T) { t.Run(test.desc, func(t *testing.T) { t.Parallel() - actual := hasRedirect(test.service) + actual := getCircuitBreaker(test.service) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetLoadBalancer(t *testing.T) { + testCases := []struct { + desc string + service rancherData + expected *types.LoadBalancer + }{ + { + desc: "should return nil when no LB labels", + service: rancherData{ + Labels: map[string]string{}, + Health: "healthy", + State: "active", + }, + expected: nil, + }, + { + desc: "should return a struct when labels are set", + service: rancherData{ + Labels: map[string]string{ + label.TraefikBackendLoadBalancerMethod: "drr", + label.TraefikBackendLoadBalancerSticky: "true", + label.TraefikBackendLoadBalancerStickiness: "true", + label.TraefikBackendLoadBalancerStickinessCookieName: "foo", + }, + Health: "healthy", + State: "active", + }, + expected: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: &types.Stickiness{ + CookieName: "foo", + }, + }, + }, + { + desc: "should return a nil Stickiness when Stickiness is not set", + service: rancherData{ + Labels: map[string]string{ + label.TraefikBackendLoadBalancerMethod: "drr", + label.TraefikBackendLoadBalancerSticky: "true", + label.TraefikBackendLoadBalancerStickinessCookieName: "foo", + }, + Health: "healthy", + State: "active", + }, + expected: &types.LoadBalancer{ + Method: "drr", + Sticky: true, + Stickiness: nil, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getLoadBalancer(test.service) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetMaxConn(t *testing.T) { + testCases := []struct { + desc string + service rancherData + expected *types.MaxConn + }{ + { + desc: "should return nil when no max conn labels", + service: rancherData{ + Labels: map[string]string{}, + Health: "healthy", + State: "active", + }, + expected: nil, + }, + { + desc: "should return nil when no amount label", + service: rancherData{ + Labels: map[string]string{ + label.TraefikBackendMaxConnExtractorFunc: "client.ip", + }, + Health: "healthy", + State: "active", + }, + expected: nil, + }, + { + desc: "should return default when no empty extractorFunc label", + service: rancherData{ + Labels: map[string]string{ + label.TraefikBackendMaxConnExtractorFunc: "", + label.TraefikBackendMaxConnAmount: "666", + }, + Health: "healthy", + State: "active", + }, + expected: &types.MaxConn{ + ExtractorFunc: "request.host", + Amount: 666, + }, + }, + { + desc: "should return a struct when max conn labels are set", + service: rancherData{ + Labels: map[string]string{ + label.TraefikBackendMaxConnExtractorFunc: "client.ip", + label.TraefikBackendMaxConnAmount: "666", + }, + Health: "healthy", + State: "active", + }, + expected: &types.MaxConn{ + ExtractorFunc: "client.ip", + Amount: 666, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getMaxConn(test.service) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetHealthCheck(t *testing.T) { + testCases := []struct { + desc string + service rancherData + expected *types.HealthCheck + }{ + { + desc: "should return nil when no health check labels", + service: rancherData{ + Labels: map[string]string{}, + Health: "healthy", + State: "active", + }, + expected: nil, + }, + { + desc: "should return nil when no health check Path label", + service: rancherData{ + Labels: map[string]string{ + label.TraefikBackendHealthCheckPort: "80", + label.TraefikBackendHealthCheckInterval: "6", + }, + Health: "healthy", + State: "active", + }, + expected: nil, + }, + { + desc: "should return a struct when health check labels are set", + service: rancherData{ + Labels: map[string]string{ + label.TraefikBackendHealthCheckPath: "/health", + label.TraefikBackendHealthCheckPort: "80", + label.TraefikBackendHealthCheckInterval: "6", + }, + Health: "healthy", + State: "active", + }, + expected: &types.HealthCheck{ + Path: "/health", + Port: 80, + Interval: "6", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getHealthCheck(test.service) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetServers(t *testing.T) { + testCases := []struct { + desc string + service rancherData + expected map[string]types.Server + }{ + { + desc: "should return nil when no server labels", + service: rancherData{ + Labels: map[string]string{}, + Health: "healthy", + State: "active", + }, + expected: nil, + }, + { + desc: "should return nil when no server IPs", + service: rancherData{ + Labels: map[string]string{ + label.TraefikWeight: "7", + }, + Containers: []string{}, + Health: "healthy", + State: "active", + }, + expected: nil, + }, + { + desc: "should use default weight when invalid weight value", + service: rancherData{ + Labels: map[string]string{ + label.TraefikWeight: "kls", + }, + Containers: []string{"10.10.10.0"}, + Health: "healthy", + State: "active", + }, + expected: map[string]types.Server{ + "server-0": { + URL: "http://10.10.10.0:", + Weight: 0, + }, + }, + }, + { + desc: "should return a map when configuration keys are defined", + service: rancherData{ + Labels: map[string]string{ + label.TraefikWeight: "6", + }, + Containers: []string{"10.10.10.0", "10.10.10.1"}, + Health: "healthy", + State: "active", + }, + expected: map[string]types.Server{ + "server-0": { + URL: "http://10.10.10.0:", + Weight: 6, + }, + "server-1": { + URL: "http://10.10.10.1:", + Weight: 6, + }, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getServers(test.service) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetRedirect(t *testing.T) { + testCases := []struct { + desc string + service rancherData + expected *types.Redirect + }{ + + { + desc: "should return nil when no redirect labels", + service: rancherData{ + Labels: map[string]string{}, + Health: "healthy", + State: "active", + }, + expected: nil, + }, + { + desc: "should use only entry point tag when mix regex redirect and entry point redirect", + service: rancherData{ + Labels: map[string]string{ + label.TraefikFrontendRedirectEntryPoint: "https", + label.TraefikFrontendRedirectRegex: "(.*)", + label.TraefikFrontendRedirectReplacement: "$1", + }, + Health: "healthy", + State: "active", + }, + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when entry point redirect label", + service: rancherData{ + Labels: map[string]string{ + label.TraefikFrontendRedirectEntryPoint: "https", + }, + Health: "healthy", + State: "active", + }, + expected: &types.Redirect{ + EntryPoint: "https", + }, + }, + { + desc: "should return a struct when regex redirect labels", + service: rancherData{ + Labels: map[string]string{ + label.TraefikFrontendRedirectRegex: "(.*)", + label.TraefikFrontendRedirectReplacement: "$1", + }, + Health: "healthy", + State: "active", + }, + expected: &types.Redirect{ + Regex: "(.*)", + Replacement: "$1", + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getRedirect(test.service) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetRateLimit(t *testing.T) { + testCases := []struct { + desc string + service rancherData + expected *types.RateLimit + }{ + { + desc: "should return nil when no rate limit labels", + service: rancherData{ + Labels: map[string]string{}, + Health: "healthy", + State: "active", + }, + expected: nil, + }, + { + desc: "should return a struct when rate limit labels are defined", + service: rancherData{ + Labels: map[string]string{ + label.TraefikFrontendRateLimitExtractorFunc: "client.ip", + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", + }, + Health: "healthy", + State: "active", + }, + expected: &types.RateLimit{ + ExtractorFunc: "client.ip", + RateSet: map[string]*types.Rate{ + "foo": { + Period: flaeg.Duration(6 * time.Second), + Average: 12, + Burst: 18, + }, + "bar": { + Period: flaeg.Duration(3 * time.Second), + Average: 6, + Burst: 9, + }, + }, + }, + }, + { + desc: "should return nil when ExtractorFunc is missing", + service: rancherData{ + Labels: map[string]string{ + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitPeriod: "6", + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitAverage: "12", + label.Prefix + label.BaseFrontendRateLimit + "foo." + label.SuffixRateLimitBurst: "18", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitPeriod: "3", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitAverage: "6", + label.Prefix + label.BaseFrontendRateLimit + "bar." + label.SuffixRateLimitBurst: "9", + }, + Health: "healthy", + State: "active", + }, + expected: nil, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getRateLimit(test.service) + + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestGetHeaders(t *testing.T) { + testCases := []struct { + desc string + service rancherData + expected *types.Headers + }{ + { + desc: "should return nil when no custom headers options are set", + service: rancherData{ + Labels: map[string]string{}, + Health: "healthy", + State: "active", + }, + expected: nil, + }, + { + desc: "should return a struct when all custom headers options are set", + service: rancherData{ + Labels: map[string]string{ + label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendSSLProxyHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", + label.TraefikFrontendAllowedHosts: "foo,bar,bor", + label.TraefikFrontendHostsProxyHeaders: "foo,bar,bor", + label.TraefikFrontendSSLHost: "foo", + label.TraefikFrontendCustomFrameOptionsValue: "foo", + label.TraefikFrontendContentSecurityPolicy: "foo", + label.TraefikFrontendPublicKey: "foo", + label.TraefikFrontendReferrerPolicy: "foo", + label.TraefikFrontendSTSSeconds: "666", + label.TraefikFrontendSSLRedirect: "true", + label.TraefikFrontendSSLTemporaryRedirect: "true", + label.TraefikFrontendSTSIncludeSubdomains: "true", + label.TraefikFrontendSTSPreload: "true", + label.TraefikFrontendForceSTSHeader: "true", + label.TraefikFrontendFrameDeny: "true", + label.TraefikFrontendContentTypeNosniff: "true", + label.TraefikFrontendBrowserXSSFilter: "true", + label.TraefikFrontendIsDevelopment: "true", + }, + Health: "healthy", + State: "active", + }, + expected: &types.Headers{ + CustomRequestHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + CustomResponseHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + SSLProxyHeaders: map[string]string{ + "Access-Control-Allow-Methods": "POST,GET,OPTIONS", + "Content-Type": "application/json; charset=utf-8", + }, + AllowedHosts: []string{"foo", "bar", "bor"}, + HostsProxyHeaders: []string{"foo", "bar", "bor"}, + SSLHost: "foo", + CustomFrameOptionsValue: "foo", + ContentSecurityPolicy: "foo", + PublicKey: "foo", + ReferrerPolicy: "foo", + STSSeconds: 666, + SSLRedirect: true, + SSLTemporaryRedirect: true, + STSIncludeSubdomains: true, + STSPreload: true, + ForceSTSHeader: true, + FrameDeny: true, + ContentTypeNosniff: true, + BrowserXSSFilter: true, + IsDevelopment: true, + }, + }, + } + + for _, test := range testCases { + test := test + t.Run(test.desc, func(t *testing.T) { + t.Parallel() + + actual := getHeaders(test.service) + assert.Equal(t, test.expected, actual) }) } diff --git a/templates/rancher.tmpl b/templates/rancher.tmpl index 965321dc6..70b9e22f2 100644 --- a/templates/rancher.tmpl +++ b/templates/rancher.tmpl @@ -1,144 +1,159 @@ -{{$backendServers := .Backends}} +{{ $backendServers := .Backends }} [backends] -{{range $backendName, $backend := .Backends}} +{{range $backendName, $backend := .Backends }} - [backends.backend-{{$backendName}}] + [backends.backend-{{ $backendName }}] - {{if hasCircuitBreakerLabel $backend}} - [backends.backend-{{$backendName}}.circuitBreaker] - expression = "{{getCircuitBreakerExpression $backend}}" + {{ $circuitBreaker := getCircuitBreaker $backend }} + {{if $circuitBreaker }} + [backends."backend-{{ $backendName }}".circuitBreaker] + expression = "{{ $circuitBreaker.Expression }}" {{end}} - {{if hasLoadBalancerLabel $backend}} - [backends.backend-{{$backendName}}.loadBalancer] - method = "{{getLoadBalancerMethod $backend}}" - sticky = {{getSticky $backend}} - {{if hasStickinessLabel $backend}} - [backends.backend-{{$backendName}}.loadBalancer.stickiness] - cookieName = "{{getStickinessCookieName $backend}}" - {{end}} + {{ $loadBalancer := getLoadBalancer $backend }} + {{if $loadBalancer }} + [backends."backend-{{ $backendName }}".loadBalancer] + method = "{{ $loadBalancer.Method }}" + sticky = {{ $loadBalancer.Sticky }} + {{if $loadBalancer.Stickiness }} + [backends."backend-{{ $backendName }}".loadBalancer.stickiness] + cookieName = "{{ $loadBalancer.Stickiness.CookieName }}" + {{end}} {{end}} - {{if hasMaxConnLabels $backend}} - [backends.backend-{{$backendName}}.maxConn] - amount = {{getMaxConnAmount $backend}} - extractorFunc = "{{getMaxConnExtractorFunc $backend}}" + {{ $maxConn := getMaxConn $backend }} + {{if $maxConn }} + [backends."backend-{{ $backendName }}".maxConn] + extractorFunc = "{{ $maxConn.ExtractorFunc }}" + amount = {{ $maxConn.Amount }} {{end}} - {{if hasHealthCheckLabels $backend}} - [backends.backend-{{$backendName}}.healthCheck] - path = "{{getHealthCheckPath $backend}}" - port = {{getHealthCheckPort $backend}} - interval = "{{getHealthCheckInterval $backend}}" + {{ $healthCheck := getHealthCheck $backend }} + {{if $healthCheck }} + [backends.backend-{{ $backendName }}.healthCheck] + path = "{{ $healthCheck.Path }}" + port = {{ $healthCheck.Port }} + interval = "{{ $healthCheck.Interval }}" {{end}} - {{range $index, $ip := $backend.Containers}} - [backends.backend-{{$backendName}}.servers.server-{{$index}}] - url = "{{getProtocol $backend}}://{{$ip}}:{{getPort $backend}}" - weight = {{getWeight $backend}} + {{range $serverName, $server := getServers $backend}} + [backends.backend-{{ $backendName }}.servers.{{ $serverName }}] + url = "{{ $server.URL }}" + weight = {{ $server.Weight }} {{end}} {{end}} [frontends] -{{range $frontendName, $service := .Frontends}} +{{range $frontendName, $service := .Frontends }} - [frontends."frontend-{{$frontendName}}"] - backend = "backend-{{getBackend $service}}" - priority = {{getPriority $service}} - passHostHeader = {{getPassHostHeader $service}} - passTLSCert = {{getPassTLSCert $service}} + [frontends."frontend-{{ $frontendName }}"] + backend = "backend-{{ getBackendName $service }}" + priority = {{ getPriority $service }} + passHostHeader = {{ getPassHostHeader $service }} + passTLSCert = {{ getPassTLSCert $service }} - entryPoints = [{{range getEntryPoints $service}} + entryPoints = [{{range getEntryPoints $service }} "{{.}}", {{end}}] - {{if getWhitelistSourceRange $service}} - whitelistSourceRange = [{{range getWhitelistSourceRange $service}} + {{ $whitelistSourceRange := getWhitelistSourceRange $service }} + {{if $whitelistSourceRange }} + whitelistSourceRange = [{{range $whitelistSourceRange }} "{{.}}", {{end}}] {{end}} - basicAuth = [{{range getBasicAuth $service}} + basicAuth = [{{range getBasicAuth $service }} "{{.}}", {{end}}] - {{if hasRedirect $service}} - [frontends."frontend-{{$frontendName}}".redirect] - entryPoint = "{{getRedirectEntryPoint $service}}" - regex = "{{getRedirectRegex $service}}" - replacement = "{{getRedirectReplacement $service}}" + {{ $redirect := getRedirect $service }} + {{if $redirect }} + [frontends."frontend-{{ $frontendName }}".redirect] + entryPoint = "{{ $redirect.EntryPoint }}" + regex = "{{ $redirect.Regex }}" + replacement = "{{ $redirect.Replacement }}" {{end}} - {{ if hasErrorPages $service }} - [frontends."frontend-{{$frontendName}}".errors] - {{ range $pageName, $page := getErrorPages $service }} - [frontends."frontend-{{$frontendName}}".errors.{{ $pageName }}] - status = [{{range $page.Status}} + {{ $errorPages := getErrorPages $service }} + {{if $errorPages }} + [frontends."frontend-{{ $frontendName }}".errors] + {{range $pageName, $page := $errorPages }} + [frontends."frontend-{{ $frontendName }}".errors.{{ $pageName }}] + status = [{{range $page.Status }} "{{.}}", {{end}}] - backend = "{{$page.Backend}}" - query = "{{$page.Query}}" + backend = "{{ $page.Backend }}" + query = "{{ $page.Query }}" {{end}} {{end}} - {{ if hasRateLimits $service }} - [frontends."frontend-{{$frontendName}}".rateLimit] - extractorFunc = "{{ getRateLimitsExtractorFunc $service }}" - [frontends."frontend-{{$frontendName}}".rateLimit.rateSet] - {{ range $limitName, $rateLimit := getRateLimits $service }} - [frontends."frontend-{{$frontendName}}".rateLimit.rateSet.{{ $limitName }}] - period = "{{ $rateLimit.Period }}" - average = {{ $rateLimit.Average }} - burst = {{ $rateLimit.Burst }} - {{end}} + {{ $rateLimit := getRateLimit $service }} + {{if $rateLimit }} + [frontends."frontend-{{ $frontendName }}".rateLimit] + extractorFunc = "{{ $rateLimit.ExtractorFunc }}" + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet] + {{ range $limitName, $limit := $rateLimit.RateSet }} + [frontends."frontend-{{ $frontendName }}".rateLimit.rateSet.{{ $limitName }}] + period = "{{ $limit.Period }}" + average = {{ $limit.Average }} + burst = {{ $limit.Burst }} + {{end}} {{end}} - {{if hasHeaders $service }} - [frontends."frontend-{{$frontendName}}".headers] - SSLRedirect = {{getSSLRedirectHeaders $service}} - SSLTemporaryRedirect = {{getSSLTemporaryRedirectHeaders $service}} - SSLHost = "{{getSSLHostHeaders $service}}" - STSSeconds = {{getSTSSecondsHeaders $service}} - STSIncludeSubdomains = {{getSTSIncludeSubdomainsHeaders $service}} - STSPreload = {{getSTSPreloadHeaders $service}} - ForceSTSHeader = {{getForceSTSHeaderHeaders $service}} - FrameDeny = {{getFrameDenyHeaders $service}} - CustomFrameOptionsValue = "{{getCustomFrameOptionsValueHeaders $service}}" - ContentTypeNosniff = {{getContentTypeNosniffHeaders $service}} - BrowserXSSFilter = {{getBrowserXSSFilterHeaders $service}} - ContentSecurityPolicy = "{{getContentSecurityPolicyHeaders $service}}" - PublicKey = "{{getPublicKeyHeaders $service}}" - ReferrerPolicy = "{{getReferrerPolicyHeaders $service}}" - IsDevelopment = {{getIsDevelopmentHeaders $service}} + {{ $headers := getHeaders $service }} + {{if $headers }} + [frontends."frontend-{{ $frontendName }}".headers] + SSLRedirect = {{ $headers.SSLRedirect }} + SSLTemporaryRedirect = {{ $headers.SSLTemporaryRedirect }} + SSLHost = "{{ $headers.SSLHost }}" + STSSeconds = {{ $headers.STSSeconds }} + STSIncludeSubdomains = {{ $headers.STSIncludeSubdomains }} + STSPreload = {{ $headers.STSPreload }} + ForceSTSHeader = {{ $headers.ForceSTSHeader }} + FrameDeny = {{ $headers.FrameDeny }} + CustomFrameOptionsValue = "{{ $headers.CustomFrameOptionsValue }}" + ContentTypeNosniff = {{ $headers.ContentTypeNosniff }} + BrowserXSSFilter = {{ $headers.BrowserXSSFilter }} + ContentSecurityPolicy = "{{ $headers.ContentSecurityPolicy }}" + PublicKey = "{{ $headers.PublicKey }}" + ReferrerPolicy = "{{ $headers.ReferrerPolicy }}" + IsDevelopment = {{ $headers.IsDevelopment }} - AllowedHosts = [{{range getAllowedHostsHeaders $service}} - "{{.}}", - {{end}}] + {{if $headers.AllowedHosts }} + AllowedHosts = [{{range $headers.AllowedHosts }} + "{{.}}", + {{end}}] + {{end}} - HostsProxyHeaders = [{{range getHostsProxyHeaders $service}} - "{{.}}", - {{end}}] + {{if $headers.HostsProxyHeaders }} + HostsProxyHeaders = [{{range $headers.HostsProxyHeaders }} + "{{.}}", + {{end}}] + {{end}} - {{if hasRequestHeaders $service}} - [frontends."frontend-{{$frontendName}}".headers.customRequestHeaders] - {{range $k, $v := getRequestHeaders $service}} - {{$k}} = "{{$v}}" + {{if $headers.CustomRequestHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.customRequestHeaders] + {{range $k, $v := $headers.CustomRequestHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.CustomResponseHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.customResponseHeaders] + {{range $k, $v := $headers.CustomResponseHeaders }} + {{$k}} = "{{$v}}" + {{end}} + {{end}} + + {{if $headers.SSLProxyHeaders }} + [frontends."frontend-{{ $frontendName }}".headers.SSLProxyHeaders] + {{range $k, $v := $headers.SSLProxyHeaders }} + {{$k}} = "{{$v}}" + {{end}} {{end}} {{end}} - {{if hasResponseHeaders $service}} - [frontends."frontend-{{$frontendName}}".headers.customResponseHeaders] - {{range $k, $v := getResponseHeaders $service}} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - {{if hasSSLProxyHeaders $service}} - [frontends."frontend-{{$frontendName}}".headers.SSLProxyHeaders] - {{range $k, $v := getSSLProxyHeaders $service}} - {{$k}} = "{{$v}}" - {{end}} - {{end}} - {{end}} [frontends."frontend-{{$frontendName}}".routes."route-frontend-{{$frontendName}}"] rule = "{{getFrontendRule $service}}"